342 lines
10 KiB
Dart
342 lines
10 KiB
Dart
/// 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"}',
|
|
);
|
|
}
|
|
}
|
|
}
|