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';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user