/// Blog Post Model for Frappe API /// /// Maps to Frappe Blog Post doctype from blog.sh API. library; import 'package:json_annotation/json_annotation.dart'; import 'package:worker/features/news/domain/entities/news_article.dart'; part 'blog_post_model.g.dart'; /// Blog Post Model /// /// Represents a blog post from Frappe ERPNext system. /// Fields match the API response from frappe.client.get_list for Blog Post doctype. @JsonSerializable() class BlogPostModel { /// Unique post identifier (docname) final String name; /// Post title final String title; /// Publication date (ISO 8601 string) @JsonKey(name: 'published_on') final String? publishedOn; /// Author/blogger name final String? blogger; /// Short introduction/excerpt @JsonKey(name: 'blog_intro') final String? blogIntro; /// Full HTML content (legacy field) final String? content; /// Full HTML content (new field from Frappe) @JsonKey(name: 'content_html') final String? contentHtml; /// Featured/meta image URL @JsonKey(name: 'meta_image') final String? metaImage; /// Meta description for SEO @JsonKey(name: 'meta_description') final String? metaDescription; /// Blog category name @JsonKey(name: 'blog_category') final String? blogCategory; /// Constructor const BlogPostModel({ required this.name, required this.title, this.publishedOn, this.blogger, this.blogIntro, this.content, this.contentHtml, this.metaImage, this.metaDescription, this.blogCategory, }); /// Create model from JSON factory BlogPostModel.fromJson(Map json) => _$BlogPostModelFromJson(json); /// Convert model to JSON Map toJson() => _$BlogPostModelToJson(this); /// Convert to domain entity (NewsArticle) /// /// Stores the original blog_category name in tags[0] for filtering purposes. NewsArticle toEntity() { // Parse published date DateTime publishedDate = DateTime.now(); if (publishedOn != null) { try { publishedDate = DateTime.parse(publishedOn!); } catch (e) { // Use current date if parsing fails publishedDate = DateTime.now(); } } // Use content_html preferentially, fall back to content final htmlContent = contentHtml ?? content; // Excerpt is ONLY from blog_intro (plain text) final excerpt = blogIntro ?? ''; // Use meta image with full URL path String imageUrl; if (metaImage != null && metaImage!.isNotEmpty) { // If meta_image starts with /, prepend the base URL if (metaImage!.startsWith('/')) { imageUrl = 'https://land.dbiz.com$metaImage'; } else if (metaImage!.startsWith('http')) { imageUrl = metaImage!; } else { imageUrl = 'https://land.dbiz.com/$metaImage'; } } else { imageUrl = 'https://via.placeholder.com/400x300?text=${Uri.encodeComponent(title)}'; } // Parse category final category = _parseCategory(blogCategory ?? ''); // Calculate reading time (rough estimate: 200 words per minute) final wordCount = (htmlContent ?? excerpt).split(' ').length; final readingTime = (wordCount / 200).ceil().clamp(1, 60); return NewsArticle( id: name, title: title, excerpt: excerpt.isNotEmpty ? (excerpt.length > 300 ? '${excerpt.substring(0, 300)}...' : excerpt) : 'Không có mô tả', content: htmlContent, imageUrl: imageUrl, category: category, publishedDate: publishedDate, viewCount: 0, // Not provided by API readingTimeMinutes: readingTime, isFeatured: false, // Not provided by API authorName: blogger, authorAvatar: null, // Not provided by API tags: blogCategory != null ? [blogCategory!] : [], // Store original category name for filtering likeCount: 0, // Not provided by API commentCount: 0, // Not provided by API shareCount: 0, // Not provided by API ); } /// Parse category from blog category name static NewsCategory _parseCategory(String categoryName) { final lower = categoryName.toLowerCase(); if (lower.contains('tin') || lower.contains('news')) { return NewsCategory.news; } else if (lower.contains('chuyên') || lower.contains('kỹ thuật') || lower.contains('professional') || lower.contains('technique')) { return NewsCategory.professional; } else if (lower.contains('dự án') || lower.contains('project') || lower.contains('công trình')) { return NewsCategory.projects; } else if (lower.contains('sự kiện') || lower.contains('event')) { return NewsCategory.events; } else if (lower.contains('khuyến mãi') || lower.contains('promotion') || lower.contains('ưu đãi')) { return NewsCategory.promotions; } return NewsCategory.news; } @override String toString() { return 'BlogPostModel(name: $name, title: $title, category: $blogCategory, ' 'publishedOn: $publishedOn, hasContentHtml: ${contentHtml != null})'; } } /// Response wrapper for Blog Post list API @JsonSerializable() class BlogPostsResponse { final List message; const BlogPostsResponse({required this.message}); factory BlogPostsResponse.fromJson(Map json) => _$BlogPostsResponseFromJson(json); Map toJson() => _$BlogPostsResponseToJson(this); }