update review api.
This commit is contained in:
221
lib/features/reviews/data/models/review_model.dart
Normal file
221
lib/features/reviews/data/models/review_model.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
79
lib/features/reviews/data/models/review_response_model.dart
Normal file
79
lib/features/reviews/data/models/review_response_model.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
@@ -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)';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user