add hour window

This commit is contained in:
2025-12-16 21:47:49 +01:00
parent bd31fb6eb0
commit dc22000c79
10 changed files with 638 additions and 62 deletions

View File

@@ -1,10 +1,8 @@
using Blueberry.Redmine.Dto;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using static Blueberry.Redmine.Dto.UserTime;
namespace Blueberry.Redmine
{
@@ -88,10 +86,14 @@ namespace Blueberry.Redmine
while (true)
{
var path = $"{endpoint}&limit={limit}&offset={offset}";
var path = "";
if(endpoint.Contains('?'))
path = $"{endpoint}&limit={limit}&offset={offset}";
else
path = $"{endpoint}?limit={limit}&offset={offset}";
var responseList = await SendRequestAsync<TResponse>(HttpMethod.Get, path, token: token)
?? throw new NullReferenceException();
?? throw new NullReferenceException();
returnList.AddRange(itemParser(responseList));
@@ -139,7 +141,7 @@ namespace Blueberry.Redmine
return fields.IssuePriorities;
}
public async Task<List<IssueList.Issue>> GetOpenIssuesByAssignee(int userId, int limit = PAGING_LIMIT, IProgress<(int, int)>? progress = null, CancellationToken ? token = null)
public async Task<List<IssueList.Issue>> GetOpenIssuesByAssigneeAsync(int userId, int limit = PAGING_LIMIT, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
{
var path = $"issues.json?assigned_to_id={userId}&status_id=open";
@@ -149,7 +151,138 @@ namespace Blueberry.Redmine
return items;
}
public async Task<List<ProjectList.Project>> GetProjects(int limit = PAGING_LIMIT, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
public async Task<List<IssueList.Issue>> GetIssuesAsync(
int? userId = null,
string? projectId = null,
int? statusId = null,
bool? isOpen = null,
DateTime? createdOn = null,
DateTime? updatedOn = null,
int limit = PAGING_LIMIT,
IProgress<(int, int)>? progress = null,
CancellationToken? token = null)
{
// Start with the base endpoint
// We use a List to build parameters cleanly to avoid formatting errors
var queryParams = new List<string>();
// 1. Handle User ID
if (userId != null)
queryParams.Add($"assigned_to_id={userId}");
// 2. Handle Project ID
if (!string.IsNullOrEmpty(projectId))
queryParams.Add($"project_id={projectId}");
// 3. Handle Status (Prioritize explicit ID, then isOpen flag, then default to open)
if (statusId != null)
{
queryParams.Add($"status_id={statusId}");
}
else if (isOpen != null)
{
queryParams.Add($"status_id={(isOpen.Value ? "open" : "closed")}");
}
else
{
// Default behavior if neither is specified (preserves your original logic)
queryParams.Add("status_id=open");
}
// 4. Handle Dates (Using >= operator for "Since")
if (createdOn != null)
{
// %3E%3D is ">="
queryParams.Add($"created_on=%3E%3D{createdOn.Value:yyyy-MM-ddTHH:mm:ssZ}");
}
if (updatedOn != null)
{
queryParams.Add($"updated_on=%3E%3D{updatedOn.Value:yyyy-MM-ddTHH:mm:ssZ}");
}
// Join the path with the query string
var queryString = string.Join("&", queryParams);
var path = $"issues.json?{queryString}";
return await SendRequestWithPagingAsync<IssueList.Root, IssueList.Issue>(
HttpMethod.Get, path, limit, (x) => x.Issues, progress, token: token);
}
public async Task<List<IssueList.Issue>> GetIssuesAsync(
int? userId = null,
string? projectId = null,
int? statusId = null,
bool? isOpen = null,
// Changed single dates to From/To pairs
DateTime? createdFrom = null,
DateTime? createdTo = null,
DateTime? updatedFrom = null,
DateTime? updatedTo = null,
int limit = PAGING_LIMIT,
IProgress<(int, int)>? progress = null,
CancellationToken? token = null)
{
var queryParams = new List<string>();
// 1. Basic Filters
if (userId != null) queryParams.Add($"assigned_to_id={userId}");
if (!string.IsNullOrEmpty(projectId)) queryParams.Add($"project_id={projectId}");
// 2. Status Logic
if (statusId != null)
{
queryParams.Add($"status_id={statusId}");
}
else if (isOpen != null)
{
queryParams.Add($"status_id={(isOpen.Value ? "open" : "closed")}");
}
else
{
queryParams.Add("status_id=open");
}
// 3. Date Filter Logic (Helper function used below)
string? createdFilter = BuildDateFilter("created_on", createdFrom, createdTo);
if (createdFilter != null) queryParams.Add(createdFilter);
string? updatedFilter = BuildDateFilter("updated_on", updatedFrom, updatedTo);
if (updatedFilter != null) queryParams.Add(updatedFilter);
// 4. Construct URL
var queryString = string.Join("&", queryParams);
var path = $"issues.json?{queryString}";
return await SendRequestWithPagingAsync<IssueList.Root, IssueList.Issue>(
HttpMethod.Get, path, limit, (x) => x.Issues, progress, token: token);
}
// Helper method to determine the correct Redmine operator
private string? BuildDateFilter(string paramName, DateTime? from, DateTime? to)
{
string format = "yyyy-MM-ddTHH:mm:ssZ"; // ISO 8601
if (from.HasValue && to.HasValue)
{
// Range: "><START|END" (URL encoded as %3E%3C)
return $"{paramName}=%3E%3C{from.Value.ToString(format)}|{to.Value.ToString(format)}";
}
else if (from.HasValue)
{
// After: ">=DATE" (URL encoded as %3E%3D)
return $"{paramName}=%3E%3D{from.Value.ToString(format)}";
}
else if (to.HasValue)
{
// Before: "<=DATE" (URL encoded as %3C%3D)
return $"{paramName}=%3C%3D{to.Value.ToString(format)}";
}
return null;
}
public async Task<List<ProjectList.Project>> GetProjectsAsync(int limit = PAGING_LIMIT, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
{
var path = $"projects.json";
@@ -159,7 +292,7 @@ namespace Blueberry.Redmine
return items;
}
public async Task<List<ProjectTrackers.Tracker>> GetTrackersForProject(string projectId, CancellationToken? token = null)
public async Task<List<ProjectTrackers.Tracker>> GetTrackersForProjectAsync(string projectId, CancellationToken? token = null)
{
var path = $"projects/{projectId}.json?include=trackers";
@@ -169,7 +302,7 @@ namespace Blueberry.Redmine
return trackers.Project.Trackers;
}
public async Task<double> GetTotalTimeForUser(int userId, DateTime start, DateTime end, int limit = PAGING_LIMIT, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
public async Task<double> GetTotalTimeForUserAsync(int userId, DateTime start, DateTime end, int limit = PAGING_LIMIT, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
{
var sText = start.ToString("yyyy-MM-dd");
var eText = end.ToString("yyyy-MM-dd");
@@ -177,14 +310,24 @@ namespace Blueberry.Redmine
var path = $"time_entries.json?from={sText}&to={eText}&user_id={userId}";
var timedata = await SendRequestWithPagingAsync<UserTime.Root, UserTime.TimeEntry>(HttpMethod.Get, path, limit, (x)=> x.TimeEntries, progress, token: token);
var timedata = await SendRequestWithPagingAsync<UserTime.Root, UserTime.TimeEntry>(HttpMethod.Get, path, limit, (x) => x.TimeEntries, progress, token: token);
var hours = timedata.Sum(x => x.Hours);
return hours;
}
public async Task<List<UserTime.TimeEntry>> GetTimeForUserAsync(int userId, DateTime start, DateTime end, int limit = PAGING_LIMIT, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
{
var sText = start.ToString("yyyy-MM-dd");
var eText = end.ToString("yyyy-MM-dd");
public async Task<DetailedIssue.Issue> GetIssue(int issueId, CancellationToken? token = null)
var path = $"time_entries.json?from={sText}&to={eText}&user_id={userId}";
return await SendRequestWithPagingAsync<UserTime.Root, UserTime.TimeEntry>(HttpMethod.Get, path, limit, (x) => x.TimeEntries, progress, token: token);
}
public async Task<DetailedIssue.Issue> GetIssueAsync(int issueId, CancellationToken? token = null)
{
var path = $"issues/{issueId}.json?include=journals";
@@ -194,7 +337,7 @@ namespace Blueberry.Redmine
return issue.Issue;
}
public async Task<IssueList.Issue> GetSimpleIssue(int issueId, CancellationToken? token = null)
public async Task<IssueList.Issue> GetSimpleIssueAsync(int issueId, CancellationToken? token = null)
{
var path = $"issues/{issueId}.json?include=journals";
@@ -217,7 +360,14 @@ namespace Blueberry.Redmine
return user.User;
}
public async Task SetIssueStatus(int issueId, int statusId, CancellationToken? token = null)
public async Task<List<UserInfo.User>> GetUsersAsync(int limit = PAGING_LIMIT, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
{
var path = "users.json";
return await SendRequestWithPagingAsync<UserList.Root, UserInfo.User>(HttpMethod.Get, path, limit, (x) => x.Users, progress, token);
}
public async Task SetIssueStatusAsync(int issueId, int statusId, CancellationToken? token = null)
{
var path = $"issues/{issueId}.json";
@@ -232,14 +382,39 @@ namespace Blueberry.Redmine
await SendRequestAsync<object>(HttpMethod.Put, path, payload, token: token);
}
public async Task<List<TimeOnIssue.TimeEntry>> GetTimeOnIssue(int issueId, int limit = 25, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
public async Task AddCommentToIssueAsync(int issueId, string comment, bool isPrivate, CancellationToken? token = null)
{
var path = $"issues/{issueId}.json";
var payload = new
{
issue = new
{
notes = comment
}
};
var privatePayload = new
{
issue = new
{
private_notes = comment
}
};
if(isPrivate)
await SendRequestAsync<object>(HttpMethod.Put, path, privatePayload, token: token);
else
await SendRequestAsync<object>(HttpMethod.Put, path, payload, token: token);
}
public async Task<List<TimeOnIssue.TimeEntry>> GetTimeOnIssueAsync(int issueId, int limit = 25, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
{
var path = $"time_entries.json?issue_id={issueId}";
var times = await SendRequestWithPagingAsync<TimeOnIssue.Root, TimeOnIssue.TimeEntry>(HttpMethod.Get, path, limit, (x)=>x.TimeEntries, progress, token: token);
return times;
}
public async Task<int> CreateNewIssue(int projectId, int trackerId, string subject, string description,
public async Task<int> CreateNewIssueAsync(int projectId, int trackerId, string subject, string description,
double estimatedHours, int priorityId, int? assigneeId = null, int? parentIssueId = null, CancellationToken? token = null)
{
var path = "issues.json";