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