using FlatRender.ContentSvc.Domain.Entities; using FlatRender.ContentSvc.Domain.Enums; using FlatRender.ContentSvc.Infrastructure.Data; using FlatRender.ContentSvc.Models.Requests; using FlatRender.ContentSvc.Models.Responses; using Microsoft.EntityFrameworkCore; namespace FlatRender.ContentSvc.Application.Services; public class CmsService(ContentDbContext db) { // ── Blogs ───────────────────────────────────────────────────────────────── public async Task> GetBlogsAsync(BlogListRequest req) { var kind = Enum.TryParse(req.Kind, true, out var k) ? k : BlogKind.Blog; var q = db.Blogs.Where(x => x.Kind == kind); if (!string.IsNullOrWhiteSpace(req.Search)) q = q.Where(x => EF.Functions.ILike(x.Title, $"%{req.Search}%")); if (req.IsPublished.HasValue) q = q.Where(x => x.IsPublished == req.IsPublished); var total = await q.LongCountAsync(); var items = await q.OrderByDescending(x => x.PublishDate ?? x.CreatedAt) .Skip((req.Page - 1) * req.PageSize).Take(req.PageSize).ToListAsync(); return new PagedResponse( items.Select(MapBlogSummary), new PaginationMeta(req.Page, req.PageSize, total, (int)Math.Ceiling((double)total / req.PageSize)) ); } public async Task GetBlogBySlugAsync(string slug) { var blog = await db.Blogs.FirstOrDefaultAsync(x => x.Slug == slug) ?? throw new KeyNotFoundException($"Blog '{slug}' not found"); await db.Blogs.Where(x => x.Id == blog.Id) .ExecuteUpdateAsync(s => s.SetProperty(x => x.ViewCount, x => x.ViewCount + 1)); return MapBlogDetail(blog); } public async Task CreateBlogAsync(BlogListRequest _, CreateBlogRequest req, Guid? authorId) { var kind = Enum.TryParse(req.Kind, true, out var k) ? k : BlogKind.Blog; var blog = new Blog { TenantId = null, Kind = kind, Slug = req.Slug, Title = req.Title, ShortDescription = req.ShortDescription, Content = req.Content, MetaTitle = req.MetaTitle, MetaDescription = req.MetaDescription, MetaKeywords = req.MetaKeywords, IncludeInSiteMap = req.IncludeInSiteMap, Image = req.Image, Cover = req.Cover, AuthorUserId = authorId, AuthorDisplayName = req.AuthorDisplayName, IsPublished = req.IsPublished, PublishDate = req.PublishDate }; db.Blogs.Add(blog); await db.SaveChangesAsync(); return MapBlogDetail(blog); } public async Task UpdateBlogAsync(Guid id, UpdateBlogRequest req) { var blog = await db.Blogs.FindAsync(id) ?? throw new KeyNotFoundException($"Blog {id} not found"); blog.Slug = req.Slug; blog.Title = req.Title; blog.ShortDescription = req.ShortDescription; blog.Content = req.Content; blog.MetaTitle = req.MetaTitle; blog.MetaDescription = req.MetaDescription; blog.MetaKeywords = req.MetaKeywords; blog.IncludeInSiteMap = req.IncludeInSiteMap; blog.Image = req.Image; blog.Cover = req.Cover; blog.AuthorDisplayName = req.AuthorDisplayName; blog.IsPublished = req.IsPublished; blog.PublishDate = req.PublishDate; blog.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); return MapBlogDetail(blog); } public async Task DeleteBlogAsync(Guid id) { var blog = await db.Blogs.FindAsync(id) ?? throw new KeyNotFoundException($"Blog {id} not found"); blog.DeletedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } // ── Comments ────────────────────────────────────────────────────────────── public async Task> GetCommentsAsync( int page, int pageSize, Guid? blogId, Guid? containerId, bool? isApproved) { var q = db.Comments.AsQueryable(); if (blogId.HasValue) q = q.Where(x => x.BlogId == blogId); if (containerId.HasValue) q = q.Where(x => x.ContainerId == containerId); if (isApproved.HasValue) q = q.Where(x => x.IsApproved == isApproved); var total = await q.LongCountAsync(); var items = await q.OrderByDescending(x => x.CreatedAt).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); return new PagedResponse( items.Select(MapComment), new PaginationMeta(page, pageSize, total, (int)Math.Ceiling((double)total / pageSize)) ); } public async Task CreateCommentAsync(CreateCommentRequest req, Guid userId) { if (req.BlogId == null && req.ContainerId == null) throw new ArgumentException("Either BlogId or ContainerId must be provided"); if (req.BlogId != null && req.ContainerId != null) throw new ArgumentException("Only one of BlogId or ContainerId may be provided"); var comment = new Comment { UserId = userId, BlogId = req.BlogId, ContainerId = req.ContainerId, ParentCommentId = req.ParentCommentId, Content = req.Content, Rate = req.Rate }; db.Comments.Add(comment); await db.SaveChangesAsync(); return MapComment(comment); } public async Task ApproveCommentAsync(Guid id, bool approve) { var comment = await db.Comments.FindAsync(id) ?? throw new KeyNotFoundException($"Comment {id} not found"); comment.IsApproved = approve; comment.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } public async Task DeleteCommentAsync(Guid id) { var comment = await db.Comments.FindAsync(id) ?? throw new KeyNotFoundException($"Comment {id} not found"); comment.DeletedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } // ── Slides ──────────────────────────────────────────────────────────────── public async Task> GetSlidesAsync(Guid? tenantId) { var q = db.NewSlides.Where(x => x.IsActive && (x.TenantId == null || x.TenantId == tenantId) && (x.ExpireDate == null || x.ExpireDate > DateTime.UtcNow)); return await q.OrderBy(x => x.Sort).Select(s => new SlideResponse( s.Id, s.Keyword, s.Title, s.Image, s.Parameter, s.SlideType.ToString(), s.ExpireDate, s.Sort, s.IsActive )).ToListAsync(); } public async Task CreateSlideAsync(CreateSlideRequest req) { if (!Enum.TryParse(req.SlideType, true, out var type)) throw new ArgumentException($"Invalid SlideType: {req.SlideType}"); var slide = new NewSlide { Keyword = req.Keyword, Title = req.Title, Image = req.Image, Parameter = req.Parameter, SlideType = type, ExpireDate = req.ExpireDate, Sort = req.Sort, IsActive = req.IsActive }; db.NewSlides.Add(slide); await db.SaveChangesAsync(); return new SlideResponse(slide.Id, slide.Keyword, slide.Title, slide.Image, slide.Parameter, slide.SlideType.ToString(), slide.ExpireDate, slide.Sort, slide.IsActive); } public async Task DeleteSlideAsync(Guid id) { var slide = await db.NewSlides.FindAsync(id) ?? throw new KeyNotFoundException($"Slide {id} not found"); db.NewSlides.Remove(slide); await db.SaveChangesAsync(); } // ── Home Page Events ────────────────────────────────────────────────────── public async Task> GetHomePageEventsAsync(Guid? tenantId) { return await db.HomePageEvents .Where(x => x.IsActive && (x.TenantId == null || x.TenantId == tenantId)) .OrderBy(x => x.Sort) .Select(e => new HomePageEventResponse( e.Id, e.Title, e.Subtitle, e.Description, e.Badge, e.BadgeClass, e.ButtonText, e.ButtonUrl, e.ButtonClass, e.Color, e.BackgroundColor, e.TextColor, e.Image, e.IsActive, e.Sort, e.StartsAt, e.EndsAt )).ToListAsync(); } // ── Website Settings ────────────────────────────────────────────────────── public async Task> GetSettingsAsync(Guid? tenantId, bool includeSecret = false) { var q = db.WebsiteSettings.Where(x => x.TenantId == tenantId); if (!includeSecret) q = q.Where(x => !x.IsSecret); return await q.Select(s => new WebsiteSettingResponse(s.Id, s.Key, s.Value, s.Description, s.IsSecret)).ToListAsync(); } public async Task UpsertSettingAsync(Guid? tenantId, UpsertWebsiteSettingRequest req) { var setting = await db.WebsiteSettings.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Key == req.Key); if (setting == null) { setting = new WebsiteSetting { TenantId = tenantId, Key = req.Key }; db.WebsiteSettings.Add(setting); } setting.Value = req.Value; setting.Description = req.Description; setting.IsSecret = req.IsSecret; setting.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); return new WebsiteSettingResponse(setting.Id, setting.Key, setting.Value, setting.Description, setting.IsSecret); } // ── Favorites ───────────────────────────────────────────────────────────── public async Task> GetFavoriteFoldersAsync(Guid userId) { return await db.FavoriteFolders .Where(x => x.UserId == userId) .Select(f => new FavoriteFolderResponse( f.Id, f.Name, f.Description, db.FavoriteContainers.Count(fc => fc.FolderId == f.Id), f.CreatedAt )).ToListAsync(); } public async Task CreateFavoriteFolderAsync(Guid userId, Guid tenantId, CreateFavoriteFolderRequest req) { var folder = new FavoriteFolder { UserId = userId, TenantId = tenantId, Name = req.Name, Description = req.Description }; db.FavoriteFolders.Add(folder); await db.SaveChangesAsync(); return new FavoriteFolderResponse(folder.Id, folder.Name, folder.Description, 0, folder.CreatedAt); } public async Task DeleteFavoriteFolderAsync(Guid userId, Guid id) { var folder = await db.FavoriteFolders.FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId) ?? throw new KeyNotFoundException($"Folder {id} not found"); db.FavoriteFolders.Remove(folder); await db.SaveChangesAsync(); } public async Task AddFavoriteContainerAsync(Guid userId, Guid tenantId, AddFavoriteContainerRequest req) { var exists = await db.FavoriteContainers.AnyAsync(x => x.UserId == userId && x.ContainerId == req.ContainerId); if (exists) return; db.FavoriteContainers.Add(new FavoriteContainer { UserId = userId, TenantId = tenantId, ContainerId = req.ContainerId, FolderId = req.FolderId, Note = req.Note }); await db.SaveChangesAsync(); } public async Task RemoveFavoriteContainerAsync(Guid userId, Guid containerId) { var fav = await db.FavoriteContainers.FirstOrDefaultAsync(x => x.UserId == userId && x.ContainerId == containerId) ?? throw new KeyNotFoundException($"Favorite not found"); db.FavoriteContainers.Remove(fav); await db.SaveChangesAsync(); } // ── Mappers ─────────────────────────────────────────────────────────────── private static BlogSummaryResponse MapBlogSummary(Blog b) => new( b.Id, b.Slug, b.Title, b.ShortDescription, b.Image, b.Cover, b.AuthorDisplayName, b.IsPublished, b.PublishDate, b.ViewCount, b.CreatedAt ); private static BlogDetailResponse MapBlogDetail(Blog b) => new( b.Id, b.Slug, b.Title, b.ShortDescription, b.Content, b.MetaTitle, b.MetaDescription, b.MetaKeywords, b.IncludeInSiteMap, b.Image, b.Cover, b.AuthorUserId, b.AuthorDisplayName, b.IsPublished, b.PublishDate, b.ViewCount, b.CreatedAt, b.UpdatedAt ); private static CommentResponse MapComment(Comment c) => new( c.Id, c.UserId, c.BlogId, c.ContainerId, c.ParentCommentId, c.Content, c.Rate, c.IsApproved, c.IsPinned, c.CreatedAt ); }