update review api.
This commit is contained in:
@@ -14,6 +14,7 @@ import 'package:worker/features/products/domain/entities/product.dart';
|
||||
import 'package:worker/features/products/presentation/providers/products_provider.dart';
|
||||
import 'package:worker/features/products/presentation/widgets/write_review/review_guidelines_card.dart';
|
||||
import 'package:worker/features/products/presentation/widgets/write_review/star_rating_selector.dart';
|
||||
import 'package:worker/features/reviews/presentation/providers/reviews_provider.dart';
|
||||
|
||||
/// Write Review Page
|
||||
///
|
||||
@@ -87,29 +88,67 @@ class _WriteReviewPageState extends ConsumerState<WriteReviewPage> {
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
// Simulate API call (TODO: Replace with actual API integration)
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
try {
|
||||
final submitUseCase = await ref.read(submitReviewProvider.future);
|
||||
|
||||
if (mounted) {
|
||||
// Show success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.circleCheck, color: AppColors.white),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text('Đánh giá của bạn đã được gửi thành công!'),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
// API expects rating on 0-5 scale directly
|
||||
// User selected 1-5 stars, pass as-is
|
||||
final apiRating = _selectedRating.toDouble();
|
||||
|
||||
await submitUseCase(
|
||||
itemId: widget.productId,
|
||||
rating: apiRating,
|
||||
comment: _contentController.text.trim(),
|
||||
);
|
||||
|
||||
// Navigate back
|
||||
context.pop();
|
||||
if (mounted) {
|
||||
// Show success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.circleCheck, color: AppColors.white),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text('Đánh giá của bạn đã được gửi thành công!'),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
|
||||
// Invalidate reviews to refresh the list
|
||||
ref.invalidate(productReviewsProvider(widget.productId));
|
||||
|
||||
// Navigate back
|
||||
context.pop();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _isSubmitting = false);
|
||||
|
||||
// Show error message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
FontAwesomeIcons.triangleExclamation,
|
||||
color: AppColors.white,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text('Lỗi: ${e.toString()}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppColors.danger,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/products/domain/entities/product.dart';
|
||||
import 'package:worker/features/products/presentation/widgets/product_detail/write_review_button.dart';
|
||||
import 'package:worker/features/reviews/domain/entities/review.dart';
|
||||
import 'package:worker/features/reviews/presentation/providers/reviews_provider.dart';
|
||||
|
||||
/// Product Tabs Section
|
||||
///
|
||||
@@ -289,13 +292,16 @@ class _SpecificationsTab extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// Reviews Tab Content
|
||||
class _ReviewsTab extends StatelessWidget {
|
||||
class _ReviewsTab extends ConsumerWidget {
|
||||
|
||||
const _ReviewsTab({required this.productId});
|
||||
final String productId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final reviewsAsync = ref.watch(productReviewsProvider(productId));
|
||||
final avgRatingAsync = ref.watch(productAverageRatingProvider(productId));
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
@@ -304,72 +310,194 @@ class _ReviewsTab extends StatelessWidget {
|
||||
// Write Review Button
|
||||
WriteReviewButton(productId: productId),
|
||||
|
||||
// Rating Overview
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF4F6F8),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Rating Score
|
||||
const Text(
|
||||
'4.8',
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryBlue,
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Reviews Content
|
||||
reviewsAsync.when(
|
||||
data: (reviews) {
|
||||
if (reviews.isEmpty) {
|
||||
// Empty state
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Rating Overview
|
||||
avgRatingAsync.when(
|
||||
data: (avgRating) => _buildRatingOverview(reviews, avgRating),
|
||||
loading: () => _buildRatingOverview(reviews, 0),
|
||||
error: (_, __) => _buildRatingOverview(reviews, 0),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Review Items
|
||||
...reviews.map((review) => _ReviewItem(review: review)),
|
||||
const SizedBox(height: 48),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
error: (error, stack) => _buildErrorState(error.toString()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Rating Details
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Stars
|
||||
Row(
|
||||
children: List.generate(
|
||||
5,
|
||||
(index) => Icon(
|
||||
index < 4 ? FontAwesomeIcons.solidStar : FontAwesomeIcons.starHalfStroke,
|
||||
color: const Color(0xFFffc107),
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Review count
|
||||
const Text(
|
||||
'125 đánh giá',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
Widget _buildRatingOverview(List<Review> reviews, double avgRating) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF4F6F8),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Rating Score
|
||||
Text(
|
||||
avgRating.toStringAsFixed(2),
|
||||
style: const TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Review Items
|
||||
..._mockReviews.map((review) => _ReviewItem(review: review)),
|
||||
const SizedBox(height: 48),
|
||||
// Rating Details
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Stars
|
||||
Row(
|
||||
children: List.generate(5, (index) {
|
||||
if (index < avgRating.floor()) {
|
||||
return const Icon(
|
||||
FontAwesomeIcons.solidStar,
|
||||
color: Color(0xFFffc107),
|
||||
size: 18,
|
||||
);
|
||||
} else if (index < avgRating) {
|
||||
return const Icon(
|
||||
FontAwesomeIcons.starHalfStroke,
|
||||
color: Color(0xFFffc107),
|
||||
size: 18,
|
||||
);
|
||||
} else {
|
||||
return const Icon(
|
||||
FontAwesomeIcons.star,
|
||||
color: Color(0xFFffc107),
|
||||
size: 18,
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Review count
|
||||
Text(
|
||||
'${reviews.length} đánh giá',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
FontAwesomeIcons.commentSlash,
|
||||
size: 48,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Chưa có đánh giá nào',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Hãy là người đầu tiên đánh giá sản phẩm này',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(String error) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
FontAwesomeIcons.circleExclamation,
|
||||
size: 48,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Không thể tải đánh giá',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error.length > 100 ? '${error.substring(0, 100)}...' : error,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Review Item Widget
|
||||
class _ReviewItem extends StatelessWidget {
|
||||
|
||||
const _ReviewItem({required this.review});
|
||||
final Map<String, dynamic> review;
|
||||
final Review review;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -408,19 +536,20 @@ class _ReviewItem extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
review['name']?.toString() ?? '',
|
||||
review.reviewerName ?? 'Người dùng',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
review['date']?.toString() ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
if (review.reviewDate != null)
|
||||
Text(
|
||||
_formatDate(review.reviewDate!),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -429,12 +558,12 @@ class _ReviewItem extends StatelessWidget {
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Rating Stars
|
||||
// Rating Stars (convert from 0-1 to 5 stars)
|
||||
Row(
|
||||
children: List.generate(
|
||||
5,
|
||||
(index) => Icon(
|
||||
index < (review['rating'] as num? ?? 0).toInt()
|
||||
index < review.starsRating
|
||||
? FontAwesomeIcons.solidStar
|
||||
: FontAwesomeIcons.star,
|
||||
color: const Color(0xFFffc107),
|
||||
@@ -447,7 +576,7 @@ class _ReviewItem extends StatelessWidget {
|
||||
|
||||
// Review Text
|
||||
Text(
|
||||
review['text']?.toString() ?? '',
|
||||
review.comment,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
@@ -458,22 +587,17 @@ class _ReviewItem extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mock review data
|
||||
final _mockReviews = [
|
||||
{
|
||||
'name': 'Nguyễn Văn A',
|
||||
'date': '2 tuần trước',
|
||||
'rating': 5,
|
||||
'text':
|
||||
'Sản phẩm chất lượng tốt, màu sắc đẹp và dễ lắp đặt. Rất hài lòng với lựa chọn này cho ngôi nhà của gia đình.',
|
||||
},
|
||||
{
|
||||
'name': 'Trần Thị B',
|
||||
'date': '1 tháng trước',
|
||||
'rating': 4,
|
||||
'text':
|
||||
'Gạch đẹp, vân gỗ rất chân thực. Giao hàng nhanh chóng và đóng gói cẩn thận.',
|
||||
},
|
||||
];
|
||||
/// Format review date for display
|
||||
String _formatDate(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(date);
|
||||
|
||||
if (diff.inDays == 0) return 'Hôm nay';
|
||||
if (diff.inDays == 1) return 'Hôm qua';
|
||||
if (diff.inDays < 7) return '${diff.inDays} ngày trước';
|
||||
if (diff.inDays < 30) return '${(diff.inDays / 7).floor()} tuần trước';
|
||||
if (diff.inDays < 365) return '${(diff.inDays / 30).floor()} tháng trước';
|
||||
return '${(diff.inDays / 365).floor()} năm trước';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
/// Remote Data Source: Reviews
|
||||
///
|
||||
/// Handles API calls for review operations.
|
||||
library;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/core/network/dio_client.dart';
|
||||
import 'package:worker/features/reviews/data/models/review_model.dart';
|
||||
import 'package:worker/features/reviews/data/models/review_response_model.dart';
|
||||
import 'package:worker/features/reviews/data/models/review_statistics_model.dart';
|
||||
|
||||
/// Reviews remote data source interface
|
||||
abstract class ReviewsRemoteDataSource {
|
||||
/// Get reviews for a product (legacy - returns only reviews)
|
||||
Future<List<ReviewModel>> getProductReviews({
|
||||
required String itemId,
|
||||
int limitPageLength = 10,
|
||||
int limitStart = 0,
|
||||
});
|
||||
|
||||
/// Get complete review response with statistics
|
||||
Future<ReviewResponseModel> getProductReviewsWithStats({
|
||||
required String itemId,
|
||||
int limitPageLength = 10,
|
||||
int limitStart = 0,
|
||||
});
|
||||
|
||||
/// Submit a review (create or update)
|
||||
Future<void> submitReview({
|
||||
required String itemId,
|
||||
required double rating,
|
||||
required String comment,
|
||||
String? name,
|
||||
});
|
||||
|
||||
/// Delete a review
|
||||
Future<void> deleteReview({required String name});
|
||||
}
|
||||
|
||||
/// Reviews remote data source implementation
|
||||
class ReviewsRemoteDataSourceImpl implements ReviewsRemoteDataSource {
|
||||
const ReviewsRemoteDataSourceImpl(this._dioClient);
|
||||
|
||||
final DioClient _dioClient;
|
||||
|
||||
// API endpoints
|
||||
static const String _getListEndpoint =
|
||||
'/api/method/building_material.building_material.api.item_feedback.get_list';
|
||||
static const String _updateEndpoint =
|
||||
'/api/method/building_material.building_material.api.item_feedback.update';
|
||||
static const String _deleteEndpoint =
|
||||
'/api/method/building_material.building_material.api.item_feedback.delete';
|
||||
|
||||
@override
|
||||
Future<List<ReviewModel>> getProductReviews({
|
||||
required String itemId,
|
||||
int limitPageLength = 10,
|
||||
int limitStart = 0,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dioClient.post(
|
||||
_getListEndpoint,
|
||||
data: {
|
||||
'limit_page_length': limitPageLength,
|
||||
'limit_start': limitStart,
|
||||
'item_id': itemId,
|
||||
},
|
||||
);
|
||||
|
||||
// Handle response
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final data = response.data;
|
||||
|
||||
// Try different response formats
|
||||
List<dynamic> reviewsJson;
|
||||
|
||||
if (data is Map<String, dynamic>) {
|
||||
// Frappe typically returns: { "message": {...} }
|
||||
if (data.containsKey('message')) {
|
||||
final message = data['message'];
|
||||
if (message is List) {
|
||||
reviewsJson = message;
|
||||
} else if (message is Map<String, dynamic>) {
|
||||
// New API format: { "message": { "feedbacks": [...], "statistics": {...} } }
|
||||
if (message.containsKey('feedbacks')) {
|
||||
reviewsJson = message['feedbacks'] as List;
|
||||
} else if (message.containsKey('data')) {
|
||||
// Alternative: { "message": { "data": [...] } }
|
||||
reviewsJson = message['data'] as List;
|
||||
} else {
|
||||
throw const ParseException(
|
||||
'Unexpected response format: message map has no feedbacks or data key',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw const ParseException(
|
||||
'Unexpected response format: message is not a list or map',
|
||||
);
|
||||
}
|
||||
} else if (data.containsKey('data')) {
|
||||
// Alternative: { "data": [...] }
|
||||
reviewsJson = data['data'] as List;
|
||||
} else if (data.containsKey('feedbacks')) {
|
||||
// Direct feedbacks key
|
||||
reviewsJson = data['feedbacks'] as List;
|
||||
} else {
|
||||
throw const ParseException(
|
||||
'Unexpected response format: no message, data, or feedbacks key',
|
||||
);
|
||||
}
|
||||
} else if (data is List) {
|
||||
// Direct list response
|
||||
reviewsJson = data;
|
||||
} else {
|
||||
throw ParseException(
|
||||
'Unexpected response format: ${data.runtimeType}',
|
||||
);
|
||||
}
|
||||
|
||||
// Parse reviews
|
||||
return reviewsJson
|
||||
.map((json) => ReviewModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
} else {
|
||||
throw NetworkException(
|
||||
'Failed to fetch reviews',
|
||||
statusCode: response.statusCode,
|
||||
data: response.data,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} on ParseException {
|
||||
rethrow;
|
||||
} catch (e, stackTrace) {
|
||||
throw UnknownException(
|
||||
'Failed to fetch reviews: ${e.toString()}',
|
||||
e,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ReviewResponseModel> getProductReviewsWithStats({
|
||||
required String itemId,
|
||||
int limitPageLength = 10,
|
||||
int limitStart = 0,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dioClient.post(
|
||||
_getListEndpoint,
|
||||
data: {
|
||||
'limit_page_length': limitPageLength,
|
||||
'limit_start': limitStart,
|
||||
'item_id': itemId,
|
||||
},
|
||||
);
|
||||
|
||||
// Handle response
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final data = response.data;
|
||||
|
||||
// Parse the message object
|
||||
if (data is Map<String, dynamic> && data.containsKey('message')) {
|
||||
final message = data['message'];
|
||||
|
||||
if (message is Map<String, dynamic>) {
|
||||
// New API format with complete response
|
||||
return ReviewResponseModel.fromJson(message);
|
||||
} else {
|
||||
throw const ParseException(
|
||||
'Unexpected response format: message is not a map',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw const ParseException(
|
||||
'Unexpected response format: no message key',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw NetworkException(
|
||||
'Failed to fetch reviews',
|
||||
statusCode: response.statusCode,
|
||||
data: response.data,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} on ParseException {
|
||||
rethrow;
|
||||
} catch (e, stackTrace) {
|
||||
throw UnknownException(
|
||||
'Failed to fetch reviews: ${e.toString()}',
|
||||
e,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> submitReview({
|
||||
required String itemId,
|
||||
required double rating,
|
||||
required String comment,
|
||||
String? name,
|
||||
}) async {
|
||||
try {
|
||||
final data = {
|
||||
'item_id': itemId,
|
||||
'rating': rating,
|
||||
'comment': comment,
|
||||
if (name != null) 'name': name,
|
||||
};
|
||||
|
||||
final response = await _dioClient.post(
|
||||
_updateEndpoint,
|
||||
data: data,
|
||||
);
|
||||
|
||||
// Handle response
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
throw NetworkException(
|
||||
'Failed to submit review',
|
||||
statusCode: response.statusCode,
|
||||
data: response.data,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e, stackTrace) {
|
||||
throw UnknownException(
|
||||
'Failed to submit review: ${e.toString()}',
|
||||
e,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteReview({required String name}) async {
|
||||
try {
|
||||
final response = await _dioClient.post(
|
||||
_deleteEndpoint,
|
||||
data: {'name': name},
|
||||
);
|
||||
|
||||
// Handle response
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw NetworkException(
|
||||
'Failed to delete review',
|
||||
statusCode: response.statusCode,
|
||||
data: response.data,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e);
|
||||
} catch (e, stackTrace) {
|
||||
throw UnknownException(
|
||||
'Failed to delete review: ${e.toString()}',
|
||||
e,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle Dio exceptions and convert to app exceptions
|
||||
Exception _handleDioException(DioException e) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return const TimeoutException();
|
||||
|
||||
case DioExceptionType.connectionError:
|
||||
return const NoInternetException();
|
||||
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = e.response?.statusCode;
|
||||
final data = e.response?.data;
|
||||
|
||||
// Extract error message from response if available
|
||||
String? errorMessage;
|
||||
if (data is Map<String, dynamic>) {
|
||||
errorMessage = data['message'] as String? ??
|
||||
data['error'] as String? ??
|
||||
data['exc'] as String?;
|
||||
}
|
||||
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
return BadRequestException(
|
||||
errorMessage ?? 'Dữ liệu không hợp lệ',
|
||||
);
|
||||
case 401:
|
||||
return UnauthorizedException(
|
||||
errorMessage ?? 'Phiên đăng nhập hết hạn. Vui lòng đăng nhập lại.',
|
||||
);
|
||||
case 403:
|
||||
return const ForbiddenException();
|
||||
case 404:
|
||||
return NotFoundException(
|
||||
errorMessage ?? 'Không tìm thấy đánh giá',
|
||||
);
|
||||
case 409:
|
||||
return ConflictException(
|
||||
errorMessage ?? 'Đánh giá đã tồn tại',
|
||||
);
|
||||
case 429:
|
||||
return const RateLimitException();
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
return ServerException(
|
||||
errorMessage ?? 'Lỗi máy chủ. Vui lòng thử lại sau.',
|
||||
statusCode,
|
||||
);
|
||||
default:
|
||||
return NetworkException(
|
||||
errorMessage ?? 'Lỗi mạng không xác định',
|
||||
statusCode: statusCode,
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
case DioExceptionType.cancel:
|
||||
return const NetworkException('Yêu cầu đã bị hủy');
|
||||
|
||||
case DioExceptionType.badCertificate:
|
||||
return const NetworkException('Lỗi chứng chỉ SSL');
|
||||
|
||||
case DioExceptionType.unknown:
|
||||
default:
|
||||
return NetworkException(
|
||||
'Lỗi kết nối: ${e.message ?? "Unknown error"}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
221
lib/features/reviews/data/models/review_model.dart
Normal file
221
lib/features/reviews/data/models/review_model.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
/// Data Model: Review
|
||||
///
|
||||
/// JSON serializable model for review data from API.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/reviews/domain/entities/review.dart';
|
||||
|
||||
/// Review data model
|
||||
///
|
||||
/// Handles JSON serialization/deserialization for review data from the API.
|
||||
///
|
||||
/// API Response format (assumed based on common Frappe patterns):
|
||||
/// ```json
|
||||
/// {
|
||||
/// "name": "ITEM-{item_id}-{user_email}",
|
||||
/// "item_id": "GIB20 G04",
|
||||
/// "rating": 0.5,
|
||||
/// "comment": "Good product",
|
||||
/// "owner": "user@example.com",
|
||||
/// "creation": "2024-11-17 10:30:00",
|
||||
/// "modified": "2024-11-17 10:30:00"
|
||||
/// }
|
||||
/// ```
|
||||
class ReviewModel {
|
||||
const ReviewModel({
|
||||
required this.name,
|
||||
required this.itemId,
|
||||
required this.rating,
|
||||
required this.comment,
|
||||
this.owner,
|
||||
this.ownerFullName,
|
||||
this.creation,
|
||||
this.modified,
|
||||
});
|
||||
|
||||
/// Unique review identifier (format: ITEM-{item_id}-{user_email})
|
||||
final String name;
|
||||
|
||||
/// Product item code
|
||||
final String itemId;
|
||||
|
||||
/// Rating (0-1 scale from API)
|
||||
final double rating;
|
||||
|
||||
/// Review comment text
|
||||
final String comment;
|
||||
|
||||
/// Email of the review owner
|
||||
final String? owner;
|
||||
|
||||
/// Full name of the review owner (if available)
|
||||
final String? ownerFullName;
|
||||
|
||||
/// ISO 8601 timestamp when review was created
|
||||
final String? creation;
|
||||
|
||||
/// ISO 8601 timestamp when review was last modified
|
||||
final String? modified;
|
||||
|
||||
/// Create model from JSON
|
||||
factory ReviewModel.fromJson(Map<String, dynamic> json) {
|
||||
// Handle nested user object if present
|
||||
String? ownerEmail;
|
||||
String? fullName;
|
||||
|
||||
if (json.containsKey('user') && json['user'] is Map<String, dynamic>) {
|
||||
final user = json['user'] as Map<String, dynamic>;
|
||||
ownerEmail = user['name'] as String?;
|
||||
fullName = user['full_name'] as String?;
|
||||
} else {
|
||||
ownerEmail = json['owner'] as String?;
|
||||
fullName = json['owner_full_name'] as String? ?? json['full_name'] as String?;
|
||||
}
|
||||
|
||||
return ReviewModel(
|
||||
name: json['name'] as String,
|
||||
itemId: json['item_id'] as String? ?? '',
|
||||
rating: (json['rating'] as num).toDouble(),
|
||||
comment: json['comment'] as String? ?? '',
|
||||
owner: ownerEmail,
|
||||
ownerFullName: fullName,
|
||||
creation: json['creation'] as String?,
|
||||
modified: json['modified'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert model to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'item_id': itemId,
|
||||
'rating': rating,
|
||||
'comment': comment,
|
||||
if (owner != null) 'owner': owner,
|
||||
if (ownerFullName != null) 'owner_full_name': ownerFullName,
|
||||
if (creation != null) 'creation': creation,
|
||||
if (modified != null) 'modified': modified,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to domain entity
|
||||
Review toEntity() {
|
||||
return Review(
|
||||
id: name,
|
||||
itemId: itemId,
|
||||
rating: rating,
|
||||
comment: comment,
|
||||
reviewerName: ownerFullName ?? _extractNameFromEmail(owner),
|
||||
reviewerEmail: owner,
|
||||
reviewDate: creation != null ? _parseDateTime(creation!) : null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create model from domain entity
|
||||
factory ReviewModel.fromEntity(Review entity) {
|
||||
return ReviewModel(
|
||||
name: entity.id,
|
||||
itemId: entity.itemId,
|
||||
rating: entity.rating,
|
||||
comment: entity.comment,
|
||||
owner: entity.reviewerEmail,
|
||||
ownerFullName: entity.reviewerName,
|
||||
creation: entity.reviewDate?.toIso8601String(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Extract name from email (fallback if full name not available)
|
||||
///
|
||||
/// Example: "john.doe@example.com" -> "John Doe"
|
||||
String? _extractNameFromEmail(String? email) {
|
||||
if (email == null) return null;
|
||||
|
||||
final username = email.split('@').first;
|
||||
final parts = username.split('.');
|
||||
|
||||
return parts
|
||||
.map((part) => part.isEmpty
|
||||
? ''
|
||||
: part[0].toUpperCase() + part.substring(1).toLowerCase())
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/// Parse datetime string from API
|
||||
///
|
||||
/// Handles common formats:
|
||||
/// - ISO 8601: "2024-11-17T10:30:00"
|
||||
/// - Frappe format: "2024-11-17 10:30:00"
|
||||
DateTime? _parseDateTime(String dateString) {
|
||||
try {
|
||||
// Try ISO 8601 first
|
||||
return DateTime.tryParse(dateString);
|
||||
} catch (e) {
|
||||
try {
|
||||
// Try replacing space with T for ISO 8601 compatibility
|
||||
final normalized = dateString.replaceFirst(' ', 'T');
|
||||
return DateTime.tryParse(normalized);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy with method
|
||||
ReviewModel copyWith({
|
||||
String? name,
|
||||
String? itemId,
|
||||
double? rating,
|
||||
String? comment,
|
||||
String? owner,
|
||||
String? ownerFullName,
|
||||
String? creation,
|
||||
String? modified,
|
||||
}) {
|
||||
return ReviewModel(
|
||||
name: name ?? this.name,
|
||||
itemId: itemId ?? this.itemId,
|
||||
rating: rating ?? this.rating,
|
||||
comment: comment ?? this.comment,
|
||||
owner: owner ?? this.owner,
|
||||
ownerFullName: ownerFullName ?? this.ownerFullName,
|
||||
creation: creation ?? this.creation,
|
||||
modified: modified ?? this.modified,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is ReviewModel &&
|
||||
other.name == name &&
|
||||
other.itemId == itemId &&
|
||||
other.rating == rating &&
|
||||
other.comment == comment &&
|
||||
other.owner == owner &&
|
||||
other.ownerFullName == ownerFullName &&
|
||||
other.creation == creation &&
|
||||
other.modified == modified;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
name,
|
||||
itemId,
|
||||
rating,
|
||||
comment,
|
||||
owner,
|
||||
ownerFullName,
|
||||
creation,
|
||||
modified,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ReviewModel(name: $name, itemId: $itemId, rating: $rating, '
|
||||
'comment: ${comment.substring(0, comment.length > 30 ? 30 : comment.length)}..., '
|
||||
'owner: $owner, creation: $creation)';
|
||||
}
|
||||
}
|
||||
79
lib/features/reviews/data/models/review_response_model.dart
Normal file
79
lib/features/reviews/data/models/review_response_model.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
/// Data Model: Review Response
|
||||
///
|
||||
/// Complete API response including reviews, statistics, and user feedback status.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/reviews/data/models/review_model.dart';
|
||||
import 'package:worker/features/reviews/data/models/review_statistics_model.dart';
|
||||
|
||||
/// Review response data model
|
||||
///
|
||||
/// Wraps the complete API response structure:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "feedbacks": [...],
|
||||
/// "is_already_feedback": false,
|
||||
/// "my_feedback": {...} or null,
|
||||
/// "statistics": {...}
|
||||
/// }
|
||||
/// ```
|
||||
class ReviewResponseModel {
|
||||
const ReviewResponseModel({
|
||||
required this.feedbacks,
|
||||
required this.isAlreadyFeedback,
|
||||
required this.statistics,
|
||||
this.myFeedback,
|
||||
});
|
||||
|
||||
/// List of all reviews/feedbacks
|
||||
final List<ReviewModel> feedbacks;
|
||||
|
||||
/// Whether current user has already submitted feedback
|
||||
final bool isAlreadyFeedback;
|
||||
|
||||
/// Current user's feedback (if exists)
|
||||
final ReviewModel? myFeedback;
|
||||
|
||||
/// Aggregate statistics
|
||||
final ReviewStatisticsModel statistics;
|
||||
|
||||
/// Create model from JSON
|
||||
factory ReviewResponseModel.fromJson(Map<String, dynamic> json) {
|
||||
final feedbacksList = json['feedbacks'] as List<dynamic>? ?? [];
|
||||
final feedbacks = feedbacksList
|
||||
.map((item) => ReviewModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
ReviewModel? myFeedback;
|
||||
if (json['my_feedback'] != null && json['my_feedback'] is Map<String, dynamic>) {
|
||||
myFeedback = ReviewModel.fromJson(json['my_feedback'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
final statistics = json['statistics'] != null && json['statistics'] is Map<String, dynamic>
|
||||
? ReviewStatisticsModel.fromJson(json['statistics'] as Map<String, dynamic>)
|
||||
: const ReviewStatisticsModel(totalFeedback: 0, averageRating: 0.0);
|
||||
|
||||
return ReviewResponseModel(
|
||||
feedbacks: feedbacks,
|
||||
isAlreadyFeedback: json['is_already_feedback'] as bool? ?? false,
|
||||
myFeedback: myFeedback,
|
||||
statistics: statistics,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert model to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'feedbacks': feedbacks.map((r) => r.toJson()).toList(),
|
||||
'is_already_feedback': isAlreadyFeedback,
|
||||
'my_feedback': myFeedback?.toJson(),
|
||||
'statistics': statistics.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ReviewResponseModel(feedbacks: ${feedbacks.length}, '
|
||||
'isAlreadyFeedback: $isAlreadyFeedback, statistics: $statistics)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/// Data Model: Review Statistics
|
||||
///
|
||||
/// JSON serializable model for review statistics from API.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/reviews/domain/entities/review_statistics.dart';
|
||||
|
||||
/// Review statistics data model
|
||||
///
|
||||
/// Handles JSON serialization/deserialization for review statistics.
|
||||
///
|
||||
/// API Response format:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "total_feedback": 2,
|
||||
/// "average_rating": 2.25
|
||||
/// }
|
||||
/// ```
|
||||
class ReviewStatisticsModel {
|
||||
const ReviewStatisticsModel({
|
||||
required this.totalFeedback,
|
||||
required this.averageRating,
|
||||
});
|
||||
|
||||
/// Total number of reviews/feedbacks
|
||||
final int totalFeedback;
|
||||
|
||||
/// Average rating (0-5 scale from API)
|
||||
final double averageRating;
|
||||
|
||||
/// Create model from JSON
|
||||
factory ReviewStatisticsModel.fromJson(Map<String, dynamic> json) {
|
||||
return ReviewStatisticsModel(
|
||||
totalFeedback: json['total_feedback'] as int? ?? 0,
|
||||
averageRating: (json['average_rating'] as num?)?.toDouble() ?? 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert model to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'total_feedback': totalFeedback,
|
||||
'average_rating': averageRating,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to domain entity
|
||||
ReviewStatistics toEntity() {
|
||||
return ReviewStatistics(
|
||||
totalFeedback: totalFeedback,
|
||||
averageRating: averageRating,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create model from domain entity
|
||||
factory ReviewStatisticsModel.fromEntity(ReviewStatistics entity) {
|
||||
return ReviewStatisticsModel(
|
||||
totalFeedback: entity.totalFeedback,
|
||||
averageRating: entity.averageRating,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is ReviewStatisticsModel &&
|
||||
other.totalFeedback == totalFeedback &&
|
||||
other.averageRating == averageRating;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(totalFeedback, averageRating);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ReviewStatisticsModel(totalFeedback: $totalFeedback, averageRating: $averageRating)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/// Repository Implementation: Reviews
|
||||
///
|
||||
/// Implements the reviews repository interface.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/reviews/data/datasources/reviews_remote_datasource.dart';
|
||||
import 'package:worker/features/reviews/domain/entities/review.dart';
|
||||
import 'package:worker/features/reviews/domain/entities/review_statistics.dart';
|
||||
import 'package:worker/features/reviews/domain/repositories/reviews_repository.dart';
|
||||
|
||||
/// Reviews repository implementation
|
||||
class ReviewsRepositoryImpl implements ReviewsRepository {
|
||||
const ReviewsRepositoryImpl(this._remoteDataSource);
|
||||
|
||||
final ReviewsRemoteDataSource _remoteDataSource;
|
||||
|
||||
@override
|
||||
Future<List<Review>> getProductReviews({
|
||||
required String itemId,
|
||||
int limitPageLength = 10,
|
||||
int limitStart = 0,
|
||||
}) async {
|
||||
final models = await _remoteDataSource.getProductReviews(
|
||||
itemId: itemId,
|
||||
limitPageLength: limitPageLength,
|
||||
limitStart: limitStart,
|
||||
);
|
||||
|
||||
// Convert models to entities and sort by date (newest first)
|
||||
final reviews = models.map((model) => model.toEntity()).toList();
|
||||
|
||||
reviews.sort((a, b) {
|
||||
if (a.reviewDate == null && b.reviewDate == null) return 0;
|
||||
if (a.reviewDate == null) return 1;
|
||||
if (b.reviewDate == null) return -1;
|
||||
return b.reviewDate!.compareTo(a.reviewDate!);
|
||||
});
|
||||
|
||||
return reviews;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ReviewStatistics> getProductReviewStatistics({
|
||||
required String itemId,
|
||||
}) async {
|
||||
final response = await _remoteDataSource.getProductReviewsWithStats(
|
||||
itemId: itemId,
|
||||
limitPageLength: 50, // Get enough reviews to calculate stats
|
||||
limitStart: 0,
|
||||
);
|
||||
|
||||
// Return statistics from API (already calculated server-side)
|
||||
return response.statistics.toEntity();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> submitReview({
|
||||
required String itemId,
|
||||
required double rating,
|
||||
required String comment,
|
||||
String? name,
|
||||
}) async {
|
||||
await _remoteDataSource.submitReview(
|
||||
itemId: itemId,
|
||||
rating: rating,
|
||||
comment: comment,
|
||||
name: name,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteReview({required String name}) async {
|
||||
await _remoteDataSource.deleteReview(name: name);
|
||||
}
|
||||
}
|
||||
116
lib/features/reviews/domain/entities/review.dart
Normal file
116
lib/features/reviews/domain/entities/review.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
/// Domain Entity: Review
|
||||
///
|
||||
/// Represents a product review with rating and comment.
|
||||
library;
|
||||
|
||||
/// Review entity
|
||||
///
|
||||
/// Contains user feedback for a product including:
|
||||
/// - Unique review ID (name field from API)
|
||||
/// - Product item code
|
||||
/// - Rating (0-1 from API, converted to 0-5 stars for display)
|
||||
/// - Review comment text
|
||||
/// - Reviewer information
|
||||
/// - Review date
|
||||
class Review {
|
||||
const Review({
|
||||
required this.id,
|
||||
required this.itemId,
|
||||
required this.rating,
|
||||
required this.comment,
|
||||
this.reviewerName,
|
||||
this.reviewerEmail,
|
||||
this.reviewDate,
|
||||
});
|
||||
|
||||
/// Unique review identifier (format: ITEM-{item_id}-{user_email})
|
||||
final String id;
|
||||
|
||||
/// Product item code being reviewed
|
||||
final String itemId;
|
||||
|
||||
/// Rating from API (0-5 scale)
|
||||
/// Note: API already provides rating on 0-5 scale, no conversion needed
|
||||
final double rating;
|
||||
|
||||
/// Review comment text
|
||||
final String comment;
|
||||
|
||||
/// Name of the reviewer (if available)
|
||||
final String? reviewerName;
|
||||
|
||||
/// Email of the reviewer (if available)
|
||||
final String? reviewerEmail;
|
||||
|
||||
/// Date when the review was created (if available)
|
||||
final DateTime? reviewDate;
|
||||
|
||||
/// Get star rating rounded to nearest integer (0-5)
|
||||
///
|
||||
/// Examples:
|
||||
/// - API rating 0.5 = 1 star
|
||||
/// - API rating 2.25 = 2 stars
|
||||
/// - API rating 4.0 = 4 stars
|
||||
int get starsRating => rating.round();
|
||||
|
||||
/// Get rating as exact decimal (0-5 scale)
|
||||
///
|
||||
/// This is useful for average rating calculations and display
|
||||
/// API already returns this on 0-5 scale
|
||||
double get starsRatingDecimal => rating;
|
||||
|
||||
/// Copy with method for creating modified copies
|
||||
Review copyWith({
|
||||
String? id,
|
||||
String? itemId,
|
||||
double? rating,
|
||||
String? comment,
|
||||
String? reviewerName,
|
||||
String? reviewerEmail,
|
||||
DateTime? reviewDate,
|
||||
}) {
|
||||
return Review(
|
||||
id: id ?? this.id,
|
||||
itemId: itemId ?? this.itemId,
|
||||
rating: rating ?? this.rating,
|
||||
comment: comment ?? this.comment,
|
||||
reviewerName: reviewerName ?? this.reviewerName,
|
||||
reviewerEmail: reviewerEmail ?? this.reviewerEmail,
|
||||
reviewDate: reviewDate ?? this.reviewDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is Review &&
|
||||
other.id == id &&
|
||||
other.itemId == itemId &&
|
||||
other.rating == rating &&
|
||||
other.comment == comment &&
|
||||
other.reviewerName == reviewerName &&
|
||||
other.reviewerEmail == reviewerEmail &&
|
||||
other.reviewDate == reviewDate;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
id,
|
||||
itemId,
|
||||
rating,
|
||||
comment,
|
||||
reviewerName,
|
||||
reviewerEmail,
|
||||
reviewDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Review(id: $id, itemId: $itemId, rating: $rating, '
|
||||
'starsRating: $starsRating, comment: ${comment.substring(0, comment.length > 30 ? 30 : comment.length)}..., '
|
||||
'reviewerName: $reviewerName, reviewDate: $reviewDate)';
|
||||
}
|
||||
}
|
||||
49
lib/features/reviews/domain/entities/review_statistics.dart
Normal file
49
lib/features/reviews/domain/entities/review_statistics.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
/// Domain Entity: Review Statistics
|
||||
///
|
||||
/// Aggregate statistics for product reviews.
|
||||
library;
|
||||
|
||||
/// Review statistics entity
|
||||
///
|
||||
/// Contains aggregate data about reviews:
|
||||
/// - Total number of feedbacks
|
||||
/// - Average rating (0-5 scale)
|
||||
class ReviewStatistics {
|
||||
const ReviewStatistics({
|
||||
required this.totalFeedback,
|
||||
required this.averageRating,
|
||||
});
|
||||
|
||||
/// Total number of reviews/feedbacks
|
||||
final int totalFeedback;
|
||||
|
||||
/// Average rating (0-5 scale)
|
||||
/// Note: This is already on 0-5 scale from API, no conversion needed
|
||||
final double averageRating;
|
||||
|
||||
/// Check if there are any reviews
|
||||
bool get hasReviews => totalFeedback > 0;
|
||||
|
||||
/// Get star rating rounded to nearest 0.5
|
||||
/// Used for displaying star icons
|
||||
double get displayRating {
|
||||
return (averageRating * 2).round() / 2;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is ReviewStatistics &&
|
||||
other.totalFeedback == totalFeedback &&
|
||||
other.averageRating == averageRating;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(totalFeedback, averageRating);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ReviewStatistics(totalFeedback: $totalFeedback, averageRating: $averageRating)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/// Domain Repository Interface: Reviews
|
||||
///
|
||||
/// Defines the contract for review data operations.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/reviews/domain/entities/review.dart';
|
||||
import 'package:worker/features/reviews/domain/entities/review_statistics.dart';
|
||||
|
||||
/// Reviews repository interface
|
||||
///
|
||||
/// Defines methods for managing product reviews:
|
||||
/// - Fetching reviews for a product
|
||||
/// - Submitting new reviews
|
||||
/// - Updating existing reviews
|
||||
/// - Deleting reviews
|
||||
abstract class ReviewsRepository {
|
||||
/// Get reviews for a specific product
|
||||
///
|
||||
/// [itemId] - Product item code
|
||||
/// [limitPageLength] - Number of reviews per page (default: 10)
|
||||
/// [limitStart] - Pagination offset (default: 0)
|
||||
///
|
||||
/// Returns a list of [Review] entities
|
||||
///
|
||||
/// Throws:
|
||||
/// - [NetworkException] on network errors
|
||||
/// - [ServerException] on server errors
|
||||
/// - [ParseException] on JSON parsing errors
|
||||
Future<List<Review>> getProductReviews({
|
||||
required String itemId,
|
||||
int limitPageLength = 10,
|
||||
int limitStart = 0,
|
||||
});
|
||||
|
||||
/// Get review statistics for a product
|
||||
///
|
||||
/// [itemId] - Product item code
|
||||
///
|
||||
/// Returns [ReviewStatistics] with total count and average rating
|
||||
///
|
||||
/// Throws:
|
||||
/// - [NetworkException] on network errors
|
||||
/// - [ServerException] on server errors
|
||||
Future<ReviewStatistics> getProductReviewStatistics({
|
||||
required String itemId,
|
||||
});
|
||||
|
||||
/// Submit a new review or update an existing one
|
||||
///
|
||||
/// [itemId] - Product item code
|
||||
/// [rating] - Rating value (0-1 scale for API)
|
||||
/// [comment] - Review comment text
|
||||
/// [name] - Optional review ID for updates (format: ITEM-{item_id}-{user_email})
|
||||
///
|
||||
/// If [name] is provided, the review will be updated.
|
||||
/// If [name] is null, a new review will be created.
|
||||
///
|
||||
/// Throws:
|
||||
/// - [NetworkException] on network errors
|
||||
/// - [ServerException] on server errors
|
||||
/// - [ValidationException] on invalid data
|
||||
/// - [AuthException] if not authenticated
|
||||
Future<void> submitReview({
|
||||
required String itemId,
|
||||
required double rating,
|
||||
required String comment,
|
||||
String? name,
|
||||
});
|
||||
|
||||
/// Delete a review
|
||||
///
|
||||
/// [name] - Review ID to delete (format: ITEM-{item_id}-{user_email})
|
||||
///
|
||||
/// Throws:
|
||||
/// - [NetworkException] on network errors
|
||||
/// - [ServerException] on server errors
|
||||
/// - [NotFoundException] if review doesn't exist
|
||||
/// - [AuthException] if not authenticated or not authorized
|
||||
Future<void> deleteReview({
|
||||
required String name,
|
||||
});
|
||||
}
|
||||
24
lib/features/reviews/domain/usecases/delete_review.dart
Normal file
24
lib/features/reviews/domain/usecases/delete_review.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
/// Use Case: Delete Review
|
||||
///
|
||||
/// Deletes a product review.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/reviews/domain/repositories/reviews_repository.dart';
|
||||
|
||||
/// Use case for deleting a product review
|
||||
class DeleteReview {
|
||||
const DeleteReview(this._repository);
|
||||
|
||||
final ReviewsRepository _repository;
|
||||
|
||||
/// Execute the use case
|
||||
///
|
||||
/// [name] - Review ID to delete (format: ITEM-{item_id}-{user_email})
|
||||
Future<void> call({required String name}) async {
|
||||
if (name.trim().isEmpty) {
|
||||
throw ArgumentError('Review ID cannot be empty');
|
||||
}
|
||||
|
||||
await _repository.deleteReview(name: name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/// Use Case: Get Product Reviews
|
||||
///
|
||||
/// Fetches reviews for a specific product with pagination.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/reviews/domain/entities/review.dart';
|
||||
import 'package:worker/features/reviews/domain/repositories/reviews_repository.dart';
|
||||
|
||||
/// Use case for getting product reviews
|
||||
class GetProductReviews {
|
||||
const GetProductReviews(this._repository);
|
||||
|
||||
final ReviewsRepository _repository;
|
||||
|
||||
/// Execute the use case
|
||||
///
|
||||
/// [itemId] - Product item code
|
||||
/// [limitPageLength] - Number of reviews per page (default: 10)
|
||||
/// [limitStart] - Pagination offset (default: 0)
|
||||
///
|
||||
/// Returns a list of [Review] entities sorted by date (newest first)
|
||||
Future<List<Review>> call({
|
||||
required String itemId,
|
||||
int limitPageLength = 10,
|
||||
int limitStart = 0,
|
||||
}) async {
|
||||
return await _repository.getProductReviews(
|
||||
itemId: itemId,
|
||||
limitPageLength: limitPageLength,
|
||||
limitStart: limitStart,
|
||||
);
|
||||
}
|
||||
}
|
||||
61
lib/features/reviews/domain/usecases/submit_review.dart
Normal file
61
lib/features/reviews/domain/usecases/submit_review.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
/// Use Case: Submit Review
|
||||
///
|
||||
/// Submits a new product review or updates an existing one.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/reviews/domain/repositories/reviews_repository.dart';
|
||||
|
||||
/// Use case for submitting a product review
|
||||
class SubmitReview {
|
||||
const SubmitReview(this._repository);
|
||||
|
||||
final ReviewsRepository _repository;
|
||||
|
||||
/// Execute the use case
|
||||
///
|
||||
/// [itemId] - Product item code
|
||||
/// [rating] - Rating value (0-1 scale for API)
|
||||
/// [comment] - Review comment text
|
||||
/// [name] - Optional review ID for updates
|
||||
///
|
||||
/// Note: The rating should be in 0-1 scale for the API.
|
||||
/// If you have a 1-5 star rating, convert it first: `stars / 5.0`
|
||||
Future<void> call({
|
||||
required String itemId,
|
||||
required double rating,
|
||||
required String comment,
|
||||
String? name,
|
||||
}) async {
|
||||
// Validate rating range (0-1)
|
||||
if (rating < 0 || rating > 1) {
|
||||
throw ArgumentError(
|
||||
'Rating must be between 0 and 1. Got: $rating. '
|
||||
'If you have a 1-5 star rating, convert it first: stars / 5.0',
|
||||
);
|
||||
}
|
||||
|
||||
// Validate comment length
|
||||
if (comment.trim().isEmpty) {
|
||||
throw ArgumentError('Review comment cannot be empty');
|
||||
}
|
||||
|
||||
if (comment.trim().length < 20) {
|
||||
throw ArgumentError(
|
||||
'Review comment must be at least 20 characters. Got: ${comment.trim().length}',
|
||||
);
|
||||
}
|
||||
|
||||
if (comment.length > 1000) {
|
||||
throw ArgumentError(
|
||||
'Review comment must not exceed 1000 characters. Got: ${comment.length}',
|
||||
);
|
||||
}
|
||||
|
||||
await _repository.submitReview(
|
||||
itemId: itemId,
|
||||
rating: rating,
|
||||
comment: comment.trim(),
|
||||
name: name,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/// Providers: Reviews
|
||||
///
|
||||
/// Riverpod providers for review management.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/core/network/dio_client.dart';
|
||||
import 'package:worker/features/reviews/data/datasources/reviews_remote_datasource.dart';
|
||||
import 'package:worker/features/reviews/data/repositories/reviews_repository_impl.dart';
|
||||
import 'package:worker/features/reviews/domain/entities/review.dart';
|
||||
import 'package:worker/features/reviews/domain/entities/review_statistics.dart';
|
||||
import 'package:worker/features/reviews/domain/repositories/reviews_repository.dart';
|
||||
import 'package:worker/features/reviews/domain/usecases/delete_review.dart';
|
||||
import 'package:worker/features/reviews/domain/usecases/get_product_reviews.dart';
|
||||
import 'package:worker/features/reviews/domain/usecases/submit_review.dart';
|
||||
|
||||
part 'reviews_provider.g.dart';
|
||||
|
||||
// ============================================================================
|
||||
// Data Layer Providers
|
||||
// ============================================================================
|
||||
|
||||
/// Provider for reviews remote data source
|
||||
@riverpod
|
||||
Future<ReviewsRemoteDataSource> reviewsRemoteDataSource(
|
||||
Ref ref,
|
||||
) async {
|
||||
final dioClient = await ref.watch(dioClientProvider.future);
|
||||
return ReviewsRemoteDataSourceImpl(dioClient);
|
||||
}
|
||||
|
||||
/// Provider for reviews repository
|
||||
@riverpod
|
||||
Future<ReviewsRepository> reviewsRepository(Ref ref) async {
|
||||
final remoteDataSource = await ref.watch(reviewsRemoteDataSourceProvider.future);
|
||||
return ReviewsRepositoryImpl(remoteDataSource);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Use Case Providers
|
||||
// ============================================================================
|
||||
|
||||
/// Provider for get product reviews use case
|
||||
@riverpod
|
||||
Future<GetProductReviews> getProductReviews(Ref ref) async {
|
||||
final repository = await ref.watch(reviewsRepositoryProvider.future);
|
||||
return GetProductReviews(repository);
|
||||
}
|
||||
|
||||
/// Provider for submit review use case
|
||||
@riverpod
|
||||
Future<SubmitReview> submitReview(Ref ref) async {
|
||||
final repository = await ref.watch(reviewsRepositoryProvider.future);
|
||||
return SubmitReview(repository);
|
||||
}
|
||||
|
||||
/// Provider for delete review use case
|
||||
@riverpod
|
||||
Future<DeleteReview> deleteReview(Ref ref) async {
|
||||
final repository = await ref.watch(reviewsRepositoryProvider.future);
|
||||
return DeleteReview(repository);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// State Providers
|
||||
// ============================================================================
|
||||
|
||||
/// Provider for fetching reviews for a specific product
|
||||
///
|
||||
/// This is a family provider that takes a product ID and returns
|
||||
/// the list of reviews for that product.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final reviewsAsync = ref.watch(productReviewsProvider('PRODUCT_ID'));
|
||||
/// ```
|
||||
@riverpod
|
||||
Future<List<Review>> productReviews(
|
||||
Ref ref,
|
||||
String itemId,
|
||||
) async {
|
||||
final getProductReviewsUseCase = await ref.watch(getProductReviewsProvider.future);
|
||||
|
||||
return await getProductReviewsUseCase(
|
||||
itemId: itemId,
|
||||
limitPageLength: 50, // Fetch more reviews
|
||||
limitStart: 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Provider for review statistics (from API)
|
||||
///
|
||||
/// Gets statistics directly from API including:
|
||||
/// - Total feedback count
|
||||
/// - Average rating (0-5 scale, already calculated by server)
|
||||
///
|
||||
/// This is more efficient than calculating client-side
|
||||
@riverpod
|
||||
Future<ReviewStatistics> productReviewStatistics(
|
||||
Ref ref,
|
||||
String itemId,
|
||||
) async {
|
||||
final repository = await ref.watch(reviewsRepositoryProvider.future);
|
||||
return await repository.getProductReviewStatistics(itemId: itemId);
|
||||
}
|
||||
|
||||
/// Provider for average rating (convenience wrapper)
|
||||
///
|
||||
/// Gets the average rating from API statistics
|
||||
/// Returns 0.0 if there are no reviews.
|
||||
@riverpod
|
||||
Future<double> productAverageRating(
|
||||
Ref ref,
|
||||
String itemId,
|
||||
) async {
|
||||
final stats = await ref.watch(productReviewStatisticsProvider(itemId).future);
|
||||
return stats.averageRating;
|
||||
}
|
||||
|
||||
/// Provider for counting reviews (convenience wrapper)
|
||||
///
|
||||
/// Gets the total count from API statistics
|
||||
@riverpod
|
||||
Future<int> productReviewCount(
|
||||
Ref ref,
|
||||
String itemId,
|
||||
) async {
|
||||
final stats = await ref.watch(productReviewStatisticsProvider(itemId).future);
|
||||
return stats.totalFeedback;
|
||||
}
|
||||
|
||||
/// Provider for checking if user can submit a review
|
||||
///
|
||||
/// This can be extended to check if user has already reviewed
|
||||
/// the product and enforce one-review-per-user policy.
|
||||
///
|
||||
/// For now, it always returns true.
|
||||
@riverpod
|
||||
Future<bool> canSubmitReview(
|
||||
Ref ref,
|
||||
String itemId,
|
||||
) async {
|
||||
// TODO: Implement logic to check if user already reviewed this product
|
||||
// This would require user email from auth state
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Convert star rating (1-5) to API rating (0-1)
|
||||
///
|
||||
/// Example:
|
||||
/// - 1 star = 0.2
|
||||
/// - 2 stars = 0.4
|
||||
/// - 3 stars = 0.6
|
||||
/// - 4 stars = 0.8
|
||||
/// - 5 stars = 1.0
|
||||
double starsToApiRating(int stars) {
|
||||
if (stars < 1 || stars > 5) {
|
||||
throw ArgumentError('Stars must be between 1 and 5. Got: $stars');
|
||||
}
|
||||
return stars / 5.0;
|
||||
}
|
||||
|
||||
/// Convert API rating (0-1) to star rating (1-5)
|
||||
///
|
||||
/// Example:
|
||||
/// - 0.2 = 1 star
|
||||
/// - 0.5 = 2.5 stars (rounded to 3)
|
||||
/// - 1.0 = 5 stars
|
||||
int apiRatingToStars(double rating) {
|
||||
if (rating < 0 || rating > 1) {
|
||||
throw ArgumentError('Rating must be between 0 and 1. Got: $rating');
|
||||
}
|
||||
return (rating * 5).round();
|
||||
}
|
||||
@@ -0,0 +1,762 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'reviews_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for reviews remote data source
|
||||
|
||||
@ProviderFor(reviewsRemoteDataSource)
|
||||
const reviewsRemoteDataSourceProvider = ReviewsRemoteDataSourceProvider._();
|
||||
|
||||
/// Provider for reviews remote data source
|
||||
|
||||
final class ReviewsRemoteDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<ReviewsRemoteDataSource>,
|
||||
ReviewsRemoteDataSource,
|
||||
FutureOr<ReviewsRemoteDataSource>
|
||||
>
|
||||
with
|
||||
$FutureModifier<ReviewsRemoteDataSource>,
|
||||
$FutureProvider<ReviewsRemoteDataSource> {
|
||||
/// Provider for reviews remote data source
|
||||
const ReviewsRemoteDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'reviewsRemoteDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$reviewsRemoteDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<ReviewsRemoteDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<ReviewsRemoteDataSource> create(Ref ref) {
|
||||
return reviewsRemoteDataSource(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$reviewsRemoteDataSourceHash() =>
|
||||
r'482e19a7e1096a814c2f3b4632866d662dbfc51a';
|
||||
|
||||
/// Provider for reviews repository
|
||||
|
||||
@ProviderFor(reviewsRepository)
|
||||
const reviewsRepositoryProvider = ReviewsRepositoryProvider._();
|
||||
|
||||
/// Provider for reviews repository
|
||||
|
||||
final class ReviewsRepositoryProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<ReviewsRepository>,
|
||||
ReviewsRepository,
|
||||
FutureOr<ReviewsRepository>
|
||||
>
|
||||
with
|
||||
$FutureModifier<ReviewsRepository>,
|
||||
$FutureProvider<ReviewsRepository> {
|
||||
/// Provider for reviews repository
|
||||
const ReviewsRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'reviewsRepositoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$reviewsRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<ReviewsRepository> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<ReviewsRepository> create(Ref ref) {
|
||||
return reviewsRepository(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$reviewsRepositoryHash() => r'4f4a7ec3d4450f0dd0cf10a05bd666444e74879b';
|
||||
|
||||
/// Provider for get product reviews use case
|
||||
|
||||
@ProviderFor(getProductReviews)
|
||||
const getProductReviewsProvider = GetProductReviewsProvider._();
|
||||
|
||||
/// Provider for get product reviews use case
|
||||
|
||||
final class GetProductReviewsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<GetProductReviews>,
|
||||
GetProductReviews,
|
||||
FutureOr<GetProductReviews>
|
||||
>
|
||||
with
|
||||
$FutureModifier<GetProductReviews>,
|
||||
$FutureProvider<GetProductReviews> {
|
||||
/// Provider for get product reviews use case
|
||||
const GetProductReviewsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'getProductReviewsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$getProductReviewsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<GetProductReviews> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<GetProductReviews> create(Ref ref) {
|
||||
return getProductReviews(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$getProductReviewsHash() => r'0ad92df95d39333aeb1b2e24946862507c911bdc';
|
||||
|
||||
/// Provider for submit review use case
|
||||
|
||||
@ProviderFor(submitReview)
|
||||
const submitReviewProvider = SubmitReviewProvider._();
|
||||
|
||||
/// Provider for submit review use case
|
||||
|
||||
final class SubmitReviewProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<SubmitReview>,
|
||||
SubmitReview,
|
||||
FutureOr<SubmitReview>
|
||||
>
|
||||
with $FutureModifier<SubmitReview>, $FutureProvider<SubmitReview> {
|
||||
/// Provider for submit review use case
|
||||
const SubmitReviewProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'submitReviewProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$submitReviewHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<SubmitReview> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<SubmitReview> create(Ref ref) {
|
||||
return submitReview(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$submitReviewHash() => r'617eaa6ccd168a597517f6a828d03100bb9508f1';
|
||||
|
||||
/// Provider for delete review use case
|
||||
|
||||
@ProviderFor(deleteReview)
|
||||
const deleteReviewProvider = DeleteReviewProvider._();
|
||||
|
||||
/// Provider for delete review use case
|
||||
|
||||
final class DeleteReviewProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<DeleteReview>,
|
||||
DeleteReview,
|
||||
FutureOr<DeleteReview>
|
||||
>
|
||||
with $FutureModifier<DeleteReview>, $FutureProvider<DeleteReview> {
|
||||
/// Provider for delete review use case
|
||||
const DeleteReviewProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'deleteReviewProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$deleteReviewHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<DeleteReview> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<DeleteReview> create(Ref ref) {
|
||||
return deleteReview(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$deleteReviewHash() => r'13b6f2529258f7db56cc1fce89a6a1af417a74b3';
|
||||
|
||||
/// Provider for fetching reviews for a specific product
|
||||
///
|
||||
/// This is a family provider that takes a product ID and returns
|
||||
/// the list of reviews for that product.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final reviewsAsync = ref.watch(productReviewsProvider('PRODUCT_ID'));
|
||||
/// ```
|
||||
|
||||
@ProviderFor(productReviews)
|
||||
const productReviewsProvider = ProductReviewsFamily._();
|
||||
|
||||
/// Provider for fetching reviews for a specific product
|
||||
///
|
||||
/// This is a family provider that takes a product ID and returns
|
||||
/// the list of reviews for that product.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final reviewsAsync = ref.watch(productReviewsProvider('PRODUCT_ID'));
|
||||
/// ```
|
||||
|
||||
final class ProductReviewsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<Review>>,
|
||||
List<Review>,
|
||||
FutureOr<List<Review>>
|
||||
>
|
||||
with $FutureModifier<List<Review>>, $FutureProvider<List<Review>> {
|
||||
/// Provider for fetching reviews for a specific product
|
||||
///
|
||||
/// This is a family provider that takes a product ID and returns
|
||||
/// the list of reviews for that product.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final reviewsAsync = ref.watch(productReviewsProvider('PRODUCT_ID'));
|
||||
/// ```
|
||||
const ProductReviewsProvider._({
|
||||
required ProductReviewsFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'productReviewsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$productReviewsHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'productReviewsProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<Review>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<Review>> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return productReviews(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ProductReviewsProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$productReviewsHash() => r'e7a4da21c3d98c2f3c297b73df943b66ef4a56d5';
|
||||
|
||||
/// Provider for fetching reviews for a specific product
|
||||
///
|
||||
/// This is a family provider that takes a product ID and returns
|
||||
/// the list of reviews for that product.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final reviewsAsync = ref.watch(productReviewsProvider('PRODUCT_ID'));
|
||||
/// ```
|
||||
|
||||
final class ProductReviewsFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<List<Review>>, String> {
|
||||
const ProductReviewsFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'productReviewsProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider for fetching reviews for a specific product
|
||||
///
|
||||
/// This is a family provider that takes a product ID and returns
|
||||
/// the list of reviews for that product.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final reviewsAsync = ref.watch(productReviewsProvider('PRODUCT_ID'));
|
||||
/// ```
|
||||
|
||||
ProductReviewsProvider call(String itemId) =>
|
||||
ProductReviewsProvider._(argument: itemId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'productReviewsProvider';
|
||||
}
|
||||
|
||||
/// Provider for review statistics (from API)
|
||||
///
|
||||
/// Gets statistics directly from API including:
|
||||
/// - Total feedback count
|
||||
/// - Average rating (0-5 scale, already calculated by server)
|
||||
///
|
||||
/// This is more efficient than calculating client-side
|
||||
|
||||
@ProviderFor(productReviewStatistics)
|
||||
const productReviewStatisticsProvider = ProductReviewStatisticsFamily._();
|
||||
|
||||
/// Provider for review statistics (from API)
|
||||
///
|
||||
/// Gets statistics directly from API including:
|
||||
/// - Total feedback count
|
||||
/// - Average rating (0-5 scale, already calculated by server)
|
||||
///
|
||||
/// This is more efficient than calculating client-side
|
||||
|
||||
final class ProductReviewStatisticsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<ReviewStatistics>,
|
||||
ReviewStatistics,
|
||||
FutureOr<ReviewStatistics>
|
||||
>
|
||||
with $FutureModifier<ReviewStatistics>, $FutureProvider<ReviewStatistics> {
|
||||
/// Provider for review statistics (from API)
|
||||
///
|
||||
/// Gets statistics directly from API including:
|
||||
/// - Total feedback count
|
||||
/// - Average rating (0-5 scale, already calculated by server)
|
||||
///
|
||||
/// This is more efficient than calculating client-side
|
||||
const ProductReviewStatisticsProvider._({
|
||||
required ProductReviewStatisticsFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'productReviewStatisticsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$productReviewStatisticsHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'productReviewStatisticsProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<ReviewStatistics> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<ReviewStatistics> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return productReviewStatistics(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ProductReviewStatisticsProvider &&
|
||||
other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$productReviewStatisticsHash() =>
|
||||
r'ed3780192e285c6ff2ac8f7ee0b7cb6f3696e2b8';
|
||||
|
||||
/// Provider for review statistics (from API)
|
||||
///
|
||||
/// Gets statistics directly from API including:
|
||||
/// - Total feedback count
|
||||
/// - Average rating (0-5 scale, already calculated by server)
|
||||
///
|
||||
/// This is more efficient than calculating client-side
|
||||
|
||||
final class ProductReviewStatisticsFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<ReviewStatistics>, String> {
|
||||
const ProductReviewStatisticsFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'productReviewStatisticsProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider for review statistics (from API)
|
||||
///
|
||||
/// Gets statistics directly from API including:
|
||||
/// - Total feedback count
|
||||
/// - Average rating (0-5 scale, already calculated by server)
|
||||
///
|
||||
/// This is more efficient than calculating client-side
|
||||
|
||||
ProductReviewStatisticsProvider call(String itemId) =>
|
||||
ProductReviewStatisticsProvider._(argument: itemId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'productReviewStatisticsProvider';
|
||||
}
|
||||
|
||||
/// Provider for average rating (convenience wrapper)
|
||||
///
|
||||
/// Gets the average rating from API statistics
|
||||
/// Returns 0.0 if there are no reviews.
|
||||
|
||||
@ProviderFor(productAverageRating)
|
||||
const productAverageRatingProvider = ProductAverageRatingFamily._();
|
||||
|
||||
/// Provider for average rating (convenience wrapper)
|
||||
///
|
||||
/// Gets the average rating from API statistics
|
||||
/// Returns 0.0 if there are no reviews.
|
||||
|
||||
final class ProductAverageRatingProvider
|
||||
extends $FunctionalProvider<AsyncValue<double>, double, FutureOr<double>>
|
||||
with $FutureModifier<double>, $FutureProvider<double> {
|
||||
/// Provider for average rating (convenience wrapper)
|
||||
///
|
||||
/// Gets the average rating from API statistics
|
||||
/// Returns 0.0 if there are no reviews.
|
||||
const ProductAverageRatingProvider._({
|
||||
required ProductAverageRatingFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'productAverageRatingProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$productAverageRatingHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'productAverageRatingProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<double> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<double> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return productAverageRating(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ProductAverageRatingProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$productAverageRatingHash() =>
|
||||
r'59e765e5004c93386999b60499430b1ae2b081a9';
|
||||
|
||||
/// Provider for average rating (convenience wrapper)
|
||||
///
|
||||
/// Gets the average rating from API statistics
|
||||
/// Returns 0.0 if there are no reviews.
|
||||
|
||||
final class ProductAverageRatingFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<double>, String> {
|
||||
const ProductAverageRatingFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'productAverageRatingProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider for average rating (convenience wrapper)
|
||||
///
|
||||
/// Gets the average rating from API statistics
|
||||
/// Returns 0.0 if there are no reviews.
|
||||
|
||||
ProductAverageRatingProvider call(String itemId) =>
|
||||
ProductAverageRatingProvider._(argument: itemId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'productAverageRatingProvider';
|
||||
}
|
||||
|
||||
/// Provider for counting reviews (convenience wrapper)
|
||||
///
|
||||
/// Gets the total count from API statistics
|
||||
|
||||
@ProviderFor(productReviewCount)
|
||||
const productReviewCountProvider = ProductReviewCountFamily._();
|
||||
|
||||
/// Provider for counting reviews (convenience wrapper)
|
||||
///
|
||||
/// Gets the total count from API statistics
|
||||
|
||||
final class ProductReviewCountProvider
|
||||
extends $FunctionalProvider<AsyncValue<int>, int, FutureOr<int>>
|
||||
with $FutureModifier<int>, $FutureProvider<int> {
|
||||
/// Provider for counting reviews (convenience wrapper)
|
||||
///
|
||||
/// Gets the total count from API statistics
|
||||
const ProductReviewCountProvider._({
|
||||
required ProductReviewCountFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'productReviewCountProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$productReviewCountHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'productReviewCountProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<int> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<int> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return productReviewCount(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ProductReviewCountProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$productReviewCountHash() =>
|
||||
r'93aba289b0a51286244ff3e4aebc417e79273113';
|
||||
|
||||
/// Provider for counting reviews (convenience wrapper)
|
||||
///
|
||||
/// Gets the total count from API statistics
|
||||
|
||||
final class ProductReviewCountFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<int>, String> {
|
||||
const ProductReviewCountFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'productReviewCountProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider for counting reviews (convenience wrapper)
|
||||
///
|
||||
/// Gets the total count from API statistics
|
||||
|
||||
ProductReviewCountProvider call(String itemId) =>
|
||||
ProductReviewCountProvider._(argument: itemId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'productReviewCountProvider';
|
||||
}
|
||||
|
||||
/// Provider for checking if user can submit a review
|
||||
///
|
||||
/// This can be extended to check if user has already reviewed
|
||||
/// the product and enforce one-review-per-user policy.
|
||||
///
|
||||
/// For now, it always returns true.
|
||||
|
||||
@ProviderFor(canSubmitReview)
|
||||
const canSubmitReviewProvider = CanSubmitReviewFamily._();
|
||||
|
||||
/// Provider for checking if user can submit a review
|
||||
///
|
||||
/// This can be extended to check if user has already reviewed
|
||||
/// the product and enforce one-review-per-user policy.
|
||||
///
|
||||
/// For now, it always returns true.
|
||||
|
||||
final class CanSubmitReviewProvider
|
||||
extends $FunctionalProvider<AsyncValue<bool>, bool, FutureOr<bool>>
|
||||
with $FutureModifier<bool>, $FutureProvider<bool> {
|
||||
/// Provider for checking if user can submit a review
|
||||
///
|
||||
/// This can be extended to check if user has already reviewed
|
||||
/// the product and enforce one-review-per-user policy.
|
||||
///
|
||||
/// For now, it always returns true.
|
||||
const CanSubmitReviewProvider._({
|
||||
required CanSubmitReviewFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'canSubmitReviewProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$canSubmitReviewHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'canSubmitReviewProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<bool> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return canSubmitReview(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is CanSubmitReviewProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$canSubmitReviewHash() => r'01506e31ea6fdf22658850750ae29416e53355bf';
|
||||
|
||||
/// Provider for checking if user can submit a review
|
||||
///
|
||||
/// This can be extended to check if user has already reviewed
|
||||
/// the product and enforce one-review-per-user policy.
|
||||
///
|
||||
/// For now, it always returns true.
|
||||
|
||||
final class CanSubmitReviewFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<bool>, String> {
|
||||
const CanSubmitReviewFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'canSubmitReviewProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider for checking if user can submit a review
|
||||
///
|
||||
/// This can be extended to check if user has already reviewed
|
||||
/// the product and enforce one-review-per-user policy.
|
||||
///
|
||||
/// For now, it always returns true.
|
||||
|
||||
CanSubmitReviewProvider call(String itemId) =>
|
||||
CanSubmitReviewProvider._(argument: itemId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'canSubmitReviewProvider';
|
||||
}
|
||||
Reference in New Issue
Block a user