PlotDirector/PlotLine/Services/FeatureRequestService.cs

176 lines
7.6 KiB
C#

using PlotLine.Data;
using PlotLine.Models;
using PlotLine.ViewModels;
namespace PlotLine.Services;
public interface IFeatureRequestService
{
Task<FeatureRequest> CreateAsync(int userId, FeatureRequestCreateViewModel model);
Task<FeatureRequest?> GetForUserAsync(int featureRequestId, int userId);
Task<FeatureRequest?> GetForAdminAsync(int featureRequestId);
Task<IReadOnlyList<FeatureRequest>> ListForUserAsync(int userId);
Task<IReadOnlyList<FeatureRequest>> ListForAdminAsync(AdminFeatureRequestFilter filter);
Task<IReadOnlyList<FeatureRequestRoadmapSection>> ListRoadmapAsync();
Task<FeatureRequestAdminSummary> GetAdminSummaryAsync();
Task<bool> UpdateAdminFieldsAsync(AdminFeatureRequestUpdateViewModel model);
Task<bool> AddUserReplyAsync(int featureRequestId, int userId, string messageBody);
Task<bool> AddAdminReplyAsync(int featureRequestId, int adminUserId, string messageBody);
Task<IReadOnlyList<FeatureRequestMessage>> ListMessagesAsync(int featureRequestId);
Task<bool> MarkViewedForUserAsync(int featureRequestId, int userId);
Task<int> CountAttentionForUserAsync(int userId);
Task<int> CountWaitingForAdminAsync();
}
public sealed class FeatureRequestService(IFeatureRequestRepository featureRequests) : IFeatureRequestService
{
public Task<FeatureRequest> CreateAsync(int userId, FeatureRequestCreateViewModel model)
{
return featureRequests.CreateAsync(
userId,
Clean(model.Title),
Clean(model.Description),
CleanNullable(model.AppArea),
CleanNullable(model.Importance));
}
public Task<FeatureRequest?> GetForUserAsync(int featureRequestId, int userId) =>
featureRequests.GetForUserAsync(featureRequestId, userId);
public Task<FeatureRequest?> GetForAdminAsync(int featureRequestId) =>
featureRequests.GetForAdminAsync(featureRequestId);
public Task<IReadOnlyList<FeatureRequest>> ListForUserAsync(int userId) =>
featureRequests.ListForUserAsync(userId);
public Task<IReadOnlyList<FeatureRequest>> ListForAdminAsync(AdminFeatureRequestFilter filter)
{
var sanitized = SanitizeFilter(filter);
return featureRequests.ListForAdminAsync(
sanitized.Status,
sanitized.AppArea,
sanitized.Importance,
sanitized.Search,
sanitized.Sort);
}
public async Task<IReadOnlyList<FeatureRequestRoadmapSection>> ListRoadmapAsync()
{
var requests = await featureRequests.ListRoadmapAsync();
return
[
BuildRoadmapSection("Under Consideration", FeatureRequestStatuses.Candidate, requests),
BuildRoadmapSection("Planned", FeatureRequestStatuses.Planned, requests),
BuildRoadmapSection("In Progress", FeatureRequestStatuses.InProgress, requests),
BuildRoadmapSection("Recently Shipped", FeatureRequestStatuses.Shipped, requests
.OrderByDescending(request => request.ShippedUtc ?? request.UpdatedUtc)
.ToList())
];
}
public async Task<FeatureRequestAdminSummary> 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<bool> UpdateAdminFieldsAsync(AdminFeatureRequestUpdateViewModel model)
{
if (!FeatureRequestStatuses.IsValid(model.Status))
{
return false;
}
var current = await featureRequests.GetForAdminAsync(model.FeatureRequestID);
if (current is null)
{
return false;
}
var statusChanged = !string.Equals(current.Status, model.Status, StringComparison.OrdinalIgnoreCase);
var roadmapVisibilityChanged = current.IsPublicCandidate != model.IsPublicCandidate;
if (statusChanged)
{
await featureRequests.UpdateStatusAsync(model.FeatureRequestID, model.Status, model.IsPublicCandidate);
}
else if (roadmapVisibilityChanged)
{
await featureRequests.UpdateRoadmapVisibilityAsync(model.FeatureRequestID, model.IsPublicCandidate);
}
await featureRequests.UpdateAdminNotesAsync(model.FeatureRequestID, CleanNullable(model.AdminNotes));
return true;
}
public async Task<bool> 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<bool> 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<IReadOnlyList<FeatureRequestMessage>> ListMessagesAsync(int featureRequestId) =>
featureRequests.ListMessagesAsync(featureRequestId);
public Task<bool> MarkViewedForUserAsync(int featureRequestId, int userId) =>
featureRequests.MarkViewedForUserAsync(featureRequestId, userId);
public Task<int> CountAttentionForUserAsync(int userId) =>
featureRequests.CountAttentionForUserAsync(userId);
public Task<int> CountWaitingForAdminAsync() =>
featureRequests.CountWaitingForAdminAsync();
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
};
}
private static FeatureRequestRoadmapSection BuildRoadmapSection(string heading, string status, IEnumerable<FeatureRequest> requests)
{
return new FeatureRequestRoadmapSection
{
Heading = heading,
Status = status,
Requests = requests
.Where(request => string.Equals(request.Status, status, StringComparison.OrdinalIgnoreCase))
.ToList()
};
}
}