update review api.

This commit is contained in:
Phuoc Nguyen
2025-11-17 17:54:32 +07:00
parent 0798b28db5
commit 0841e3bf3d
23 changed files with 4856 additions and 209 deletions

View File

@@ -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,
),
);
}
}
}

View File

@@ -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';
}
}

View File

@@ -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"}',
);
}
}
}

View 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)';
}
}

View 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)';
}
}

View File

@@ -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)';
}
}

View File

@@ -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);
}
}

View 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)';
}
}

View 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)';
}
}

View File

@@ -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,
});
}

View 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);
}
}

View File

@@ -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,
);
}
}

View 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,
);
}
}

View File

@@ -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();
}

View File

@@ -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';
}