using PlotLine.Data; using PlotLine.Models; using PlotLine.ViewModels; namespace PlotLine.Services; public interface IFeatureRequestService { Task CreateAsync(int userId, FeatureRequestCreateViewModel model); Task GetForUserAsync(int featureRequestId, int userId); Task GetForAdminAsync(int featureRequestId); Task> ListForUserAsync(int userId); Task> ListForAdminAsync(AdminFeatureRequestFilter filter); Task GetAdminSummaryAsync(); Task UpdateAdminFieldsAsync(AdminFeatureRequestUpdateViewModel model); Task AddUserReplyAsync(int featureRequestId, int userId, string messageBody); Task AddAdminReplyAsync(int featureRequestId, int adminUserId, string messageBody); Task> ListMessagesAsync(int featureRequestId); } public sealed class FeatureRequestService(IFeatureRequestRepository featureRequests) : IFeatureRequestService { public Task CreateAsync(int userId, FeatureRequestCreateViewModel model) { return featureRequests.CreateAsync( userId, Clean(model.Title), Clean(model.Description), CleanNullable(model.AppArea), CleanNullable(model.Importance)); } public Task GetForUserAsync(int featureRequestId, int userId) => featureRequests.GetForUserAsync(featureRequestId, userId); public Task GetForAdminAsync(int featureRequestId) => featureRequests.GetForAdminAsync(featureRequestId); public Task> ListForUserAsync(int userId) => featureRequests.ListForUserAsync(userId); public Task> ListForAdminAsync(AdminFeatureRequestFilter filter) { var sanitized = SanitizeFilter(filter); return featureRequests.ListForAdminAsync( sanitized.Status, sanitized.AppArea, sanitized.Importance, sanitized.Search, sanitized.Sort); } public async Task GetAdminSummaryAsync() { var requests = await featureRequests.ListForAdminAsync( status: null, appArea: null, importance: null, search: null, sort: FeatureRequestSortOptions.LatestActivity); return new FeatureRequestAdminSummary { Statuses = FeatureRequestStatuses.All .Select(status => new FeatureRequestStatusSummary { Status = status, Count = requests.Count(request => string.Equals(request.Status, status, StringComparison.OrdinalIgnoreCase)) }) .ToList(), TotalOpenRequests = requests.Count(request => request.IsOpen), AwaitingAdminReply = requests.Count(request => request.ConversationState == FeatureRequestConversationStates.WaitingForAdminReply), AwaitingUserReply = requests.Count(request => request.ConversationState == FeatureRequestConversationStates.WaitingForUserReply) }; } public async Task UpdateAdminFieldsAsync(AdminFeatureRequestUpdateViewModel model) { if (!FeatureRequestStatuses.IsValid(model.Status)) { return false; } var updated = await featureRequests.UpdateStatusAsync(model.FeatureRequestID, model.Status, model.IsPublicCandidate); if (updated is null) { return false; } await featureRequests.UpdateAdminNotesAsync(model.FeatureRequestID, CleanNullable(model.AdminNotes)); return true; } public async Task AddUserReplyAsync(int featureRequestId, int userId, string messageBody) { var message = await featureRequests.AddMessageAsync(featureRequestId, userId, isAdmin: false, Clean(messageBody)); return message is not null; } public async Task AddAdminReplyAsync(int featureRequestId, int adminUserId, string messageBody) { var message = await featureRequests.AddMessageAsync(featureRequestId, adminUserId, isAdmin: true, Clean(messageBody)); return message is not null; } public Task> ListMessagesAsync(int featureRequestId) => featureRequests.ListMessagesAsync(featureRequestId); private static string Clean(string value) => value.Trim(); private static string? CleanNullable(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); private static AdminFeatureRequestFilter SanitizeFilter(AdminFeatureRequestFilter filter) { return new AdminFeatureRequestFilter { Status = FeatureRequestStatuses.IsValid(filter.Status) ? filter.Status : null, AppArea = FeatureRequestOptions.AppAreas.Contains(filter.AppArea ?? string.Empty, StringComparer.OrdinalIgnoreCase) ? filter.AppArea : null, Importance = FeatureRequestOptions.ImportanceOptions.Contains(filter.Importance ?? string.Empty, StringComparer.OrdinalIgnoreCase) ? filter.Importance : null, Search = CleanNullable(filter.Search), Sort = FeatureRequestSortOptions.IsValid(filter.Sort) ? filter.Sort : FeatureRequestSortOptions.LatestActivity }; } }