/// 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> getProductReviews({ required String itemId, int limitPageLength = 10, int limitStart = 0, }); /// Get complete review response with statistics Future getProductReviewsWithStats({ required String itemId, int limitPageLength = 10, int limitStart = 0, }); /// Submit a review (create or update) Future submitReview({ required String itemId, required double rating, required String comment, String? name, }); /// Delete a review Future 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> 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 reviewsJson; if (data is Map) { // Frappe typically returns: { "message": {...} } if (data.containsKey('message')) { final message = data['message']; if (message is List) { reviewsJson = message; } else if (message is Map) { // 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)) .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 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 && data.containsKey('message')) { final message = data['message']; if (message is Map) { // 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 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 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) { 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"}', ); } } }