Files
Blueberry/Blueberry/Redmine/RedmineManager.cs
2025-12-10 19:10:00 +01:00

207 lines
9.1 KiB
C#

using Microsoft.Extensions.Logging;
using System.Net.Http;
using static BlueMine.Redmine.RedmineDto;
namespace BlueMine.Redmine
{
public class RedmineManager : IRedmineManager
{
private readonly RedmineConnect _redmineConnect;
private readonly AsyncLock _lock = new();
private readonly RedmineCache<SimpleProject> _projectCache;
private readonly RedmineCache<SimpleIssue> _issueCache;
private readonly ILogger<RedmineManager> _logger;
public RedmineManager(HttpClient httpClient, ILoggerFactory loggerFactory, RedmineConfig config)
{
ArgumentNullException.ThrowIfNull(httpClient);
ArgumentNullException.ThrowIfNull(loggerFactory);
ArgumentNullException.ThrowIfNull(config);
_logger = loggerFactory.CreateLogger<RedmineManager>();
_logger.LogDebug("Initializing RedmineManager with URL: {Url}", config.RedmineUrl);
_redmineConnect = new RedmineConnect(httpClient, loggerFactory.CreateLogger<RedmineConnect>(), config);
_projectCache = new(config.ProjectCacheDuration, loggerFactory.CreateLogger<RedmineCache<SimpleProject>>());
_issueCache = new(config.IssueCacheDuration, loggerFactory.CreateLogger<RedmineCache<SimpleIssue>>());
_logger.LogDebug("RedmineManager initialized");
}
/// <summary>
/// Checks if the Redmine instance is available by verifying the API key.
/// </summary>
/// <returns>True if available, false otherwise.</returns>
public async Task<bool> IsRedmineAvailable()
{
_logger.LogDebug("Checking if Redmine is available");
try
{
using (await _lock.LockAsync())
{
await _redmineConnect.VerifyApiKey();
}
_logger.LogDebug("Redmine is available");
return true;
}
catch (Exception ex)
{
_logger.LogDebug("Redmine not available: {Message}", ex.Message);
return false;
}
}
/// <summary>
/// Logs time for a specific issue.
/// </summary>
/// <param name="issueId">The issue ID.</param>
/// <param name="hours">Hours to log.</param>
/// <param name="comments">Comments for the time entry.</param>
/// <param name="date">Date of the time entry.</param>
/// <param name="activityId">Optional activity ID.</param>
public async Task LogTimeAsync(int issueId, double hours, string comments, DateTime date, int? activityId = null)
{
_logger.LogDebug("Logging {Hours} hours for issue {IssueId} on {Date}", hours, issueId, date.ToShortDateString());
using (await _lock.LockAsync())
{
await _redmineConnect.LogTimeAsync(issueId, hours, comments, date, activityId);
}
_logger.LogDebug("Time logged successfully");
}
/// <summary>
/// Closes the specified issue.
/// </summary>
/// <param name="issueId">The issue ID to close.</param>
public async Task CloseIssueAsync(int issueId)
{
_logger.LogDebug("Closing issue {IssueId}", issueId);
using (await _lock.LockAsync())
{
await _redmineConnect.CloseIssueAsync(issueId);
}
_logger.LogDebug("Issue {IssueId} closed", issueId);
}
/// <summary>
/// Creates a new issue in the specified project.
/// </summary>
/// <param name="projectId">The project ID.</param>
/// <param name="trackerId">The tracker ID.</param>
/// <param name="subject">Issue subject.</param>
/// <param name="description">Optional description.</param>
/// <param name="estimatedHours">Optional estimated hours.</param>
/// <param name="priorityId">Optional priority ID.</param>
/// <param name="parentIssueId">Optional parent issue ID.</param>
/// <returns>The created issue ID.</returns>
public async Task<int> CreateIssueAsync(string projectId, int trackerId, string subject, string? description = null,
double? estimatedHours = null, int? priorityId = 9, int? parentIssueId = null)
{
_logger.LogDebug("Creating issue in project {ProjectId} with subject '{Subject}'", projectId, subject);
using (await _lock.LockAsync())
{
var issueId = await _redmineConnect.CreateIssueAsync(projectId, trackerId, subject, description,
estimatedHours, priorityId, parentIssueId);
_logger.LogDebug("Issue created with ID {IssueId}", issueId);
return issueId;
}
}
/// <summary>
/// Retrieves the list of projects, using cache if valid.
/// </summary>
/// <param name="limit">Maximum number of projects to fetch per request.</param>
/// <param name="progress">Optional progress reporter.</param>
/// <returns>List of simple projects.</returns>
public async Task<List<SimpleProject>> GetProjectsAsync(int limit = 100, IProgress<(int, int)>? progress = null)
{
_logger.LogDebug("Getting projects");
using (await _lock.LockAsync())
{
List<SimpleProject> projects = [];
if(!_projectCache.IsCacheValid())
{
_logger.LogDebug("Cache invalid, refreshing");
_projectCache.RefreshCache([..(await _redmineConnect.GetProjectsAsync(limit, progress))]);
}
else
{
_logger.LogDebug("Using cached projects");
}
projects = _projectCache.GetItems();
_logger.LogDebug("Retrieved {Count} projects", projects.Count);
return projects;
}
}
/// <summary>
/// Retrieves trackers for the specified project.
/// </summary>
/// <param name="projectId">The project ID.</param>
/// <param name="token">Optional cancellation token.</param>
/// <returns>List of simple trackers.</returns>
public async Task<List<SimpleTracker>> GetTrackersAsync(string projectId, CancellationToken? token = null)
{
_logger.LogDebug("Getting trackers for project {ProjectId}", projectId);
try
{
using (await _lock.LockAsync())
{
var trackers = await _redmineConnect.GetTrackersAsync(projectId, token);
_logger.LogDebug("Retrieved {Count} trackers", trackers.Count);
return trackers;
}
}
catch (OperationCanceledException)
{
_logger.LogDebug("GetTrackersAsync cancelled");
throw;
}
}
/// <summary>
/// Retrieves current issues with spent time.
/// </summary>
/// <param name="progress">Optional progress reporter.</param>
/// <returns>List of issue items.</returns>
public async Task<List<IssueItem>> GetCurrentIssuesAsync(IProgress<(int, int)>? progress = null)
{
_logger.LogDebug("Getting current issues");
using (await _lock.LockAsync())
{
List<SimpleIssue> simpleIssues;
if (!_issueCache.IsCacheValid())
{
_logger.LogDebug("Issue cache invalid, refreshing");
simpleIssues = [.. (await _redmineConnect.GetMyIssuesAsync())];
_issueCache.RefreshCache(simpleIssues);
}
else
{
_logger.LogDebug("Using cached issues");
simpleIssues = _issueCache.GetItems();
}
var issues = await _redmineConnect.GetSpentTimeForIssuesAsync(simpleIssues, progress);
_logger.LogDebug("Retrieved {Count} issues", issues.Count);
return issues;
}
}
/// <summary>
/// Retrieves logged hours for the specified date range.
/// </summary>
/// <param name="startDate">Start date.</param>
/// <param name="endDate">End date.</param>
/// <returns>Total logged hours.</returns>
public async Task<double> GetLoggedHoursAsync(DateTime? startDate = null, DateTime? endDate = null)
{
var start = DateTime.Today;
var end = DateTime.Today;
if (startDate.HasValue)
start = startDate.Value;
if(endDate.HasValue)
end = endDate.Value;
_logger.LogDebug("Getting logged hours from {Start} to {End}", start.ToShortDateString(), end.ToShortDateString());
using (await _lock.LockAsync())
{
var hours = await _redmineConnect.GetTodaysHoursAsync(start, end);
_logger.LogDebug("Retrieved {Hours} hours", hours);
return hours;
}
}
}
}