add hour window
This commit is contained in:
@@ -5,18 +5,6 @@ namespace Blueberry.Redmine.Dto
|
|||||||
{
|
{
|
||||||
public class UserInfo
|
public class UserInfo
|
||||||
{
|
{
|
||||||
public class CustomField
|
|
||||||
{
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("value")]
|
|
||||||
public string Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Root
|
public class Root
|
||||||
{
|
{
|
||||||
[JsonPropertyName("user")]
|
[JsonPropertyName("user")]
|
||||||
@@ -39,6 +27,7 @@ namespace Blueberry.Redmine.Dto
|
|||||||
|
|
||||||
[JsonPropertyName("lastname")]
|
[JsonPropertyName("lastname")]
|
||||||
public string Lastname { get; set; }
|
public string Lastname { get; set; }
|
||||||
|
public string FullName => Lastname + " " + Firstname;
|
||||||
|
|
||||||
[JsonPropertyName("mail")]
|
[JsonPropertyName("mail")]
|
||||||
public string Mail { get; set; }
|
public string Mail { get; set; }
|
||||||
@@ -50,22 +39,7 @@ namespace Blueberry.Redmine.Dto
|
|||||||
public DateTime UpdatedOn { get; set; }
|
public DateTime UpdatedOn { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("last_login_on")]
|
[JsonPropertyName("last_login_on")]
|
||||||
public DateTime LastLoginOn { get; set; }
|
public DateTime? LastLoginOn { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("passwd_changed_on")]
|
|
||||||
public DateTime PasswdChangedOn { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("twofa_scheme")]
|
|
||||||
public object TwofaScheme { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("api_key")]
|
|
||||||
public string ApiKey { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("status")]
|
|
||||||
public int Status { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("custom_fields")]
|
|
||||||
public List<CustomField> CustomFields { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
24
Blueberry.Redmine/Dto/UserList.cs
Normal file
24
Blueberry.Redmine/Dto/UserList.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Blueberry.Redmine.Dto
|
||||||
|
{
|
||||||
|
public class UserList
|
||||||
|
{
|
||||||
|
public class Root : IResponseList
|
||||||
|
{
|
||||||
|
[JsonPropertyName("users")]
|
||||||
|
public List<UserInfo.User> Users { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("total_count")]
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("offset")]
|
||||||
|
public int Offset { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("limit")]
|
||||||
|
public int Limit { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
using Blueberry.Redmine.Dto;
|
using Blueberry.Redmine.Dto;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using static Blueberry.Redmine.Dto.UserTime;
|
|
||||||
|
|
||||||
namespace Blueberry.Redmine
|
namespace Blueberry.Redmine
|
||||||
{
|
{
|
||||||
@@ -88,7 +86,11 @@ namespace Blueberry.Redmine
|
|||||||
|
|
||||||
while (true)
|
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)
|
var responseList = await SendRequestAsync<TResponse>(HttpMethod.Get, path, token: token)
|
||||||
?? throw new NullReferenceException();
|
?? throw new NullReferenceException();
|
||||||
@@ -139,7 +141,7 @@ namespace Blueberry.Redmine
|
|||||||
return fields.IssuePriorities;
|
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";
|
var path = $"issues.json?assigned_to_id={userId}&status_id=open";
|
||||||
|
|
||||||
@@ -149,7 +151,138 @@ namespace Blueberry.Redmine
|
|||||||
return items;
|
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";
|
var path = $"projects.json";
|
||||||
|
|
||||||
@@ -159,7 +292,7 @@ namespace Blueberry.Redmine
|
|||||||
return items;
|
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";
|
var path = $"projects/{projectId}.json?include=trackers";
|
||||||
|
|
||||||
@@ -169,7 +302,7 @@ namespace Blueberry.Redmine
|
|||||||
return trackers.Project.Trackers;
|
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 sText = start.ToString("yyyy-MM-dd");
|
||||||
var eText = end.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 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);
|
var hours = timedata.Sum(x => x.Hours);
|
||||||
|
|
||||||
return 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";
|
var path = $"issues/{issueId}.json?include=journals";
|
||||||
|
|
||||||
@@ -194,7 +337,7 @@ namespace Blueberry.Redmine
|
|||||||
return issue.Issue;
|
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";
|
var path = $"issues/{issueId}.json?include=journals";
|
||||||
|
|
||||||
@@ -217,7 +360,14 @@ namespace Blueberry.Redmine
|
|||||||
return user.User;
|
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";
|
var path = $"issues/{issueId}.json";
|
||||||
|
|
||||||
@@ -232,14 +382,39 @@ namespace Blueberry.Redmine
|
|||||||
await SendRequestAsync<object>(HttpMethod.Put, path, payload, token: token);
|
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 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);
|
var times = await SendRequestWithPagingAsync<TimeOnIssue.Root, TimeOnIssue.TimeEntry>(HttpMethod.Get, path, limit, (x)=>x.TimeEntries, progress, token: token);
|
||||||
return times;
|
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)
|
double estimatedHours, int priorityId, int? assigneeId = null, int? parentIssueId = null, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
var path = "issues.json";
|
var path = "issues.json";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Blueberry.Redmine
|
|||||||
{
|
{
|
||||||
public class RedmineManager
|
public class RedmineManager
|
||||||
{
|
{
|
||||||
private readonly TimeSpan DEFAULT_CACHE_DURATION = TimeSpan.FromHours(1);
|
private readonly TimeSpan DEFAULT_CACHE_DURATION = TimeSpan.FromHours(3);
|
||||||
private readonly RedmineConfig _config;
|
private readonly RedmineConfig _config;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly RedmineApiClient _apiClient;
|
private readonly RedmineApiClient _apiClient;
|
||||||
@@ -13,6 +13,7 @@ namespace Blueberry.Redmine
|
|||||||
private readonly RedmineCache<PriorityList.IssuePriority> _priorityCache;
|
private readonly RedmineCache<PriorityList.IssuePriority> _priorityCache;
|
||||||
private readonly RedmineCache<CustomFieldList.CustomField> _customFieldCache;
|
private readonly RedmineCache<CustomFieldList.CustomField> _customFieldCache;
|
||||||
private readonly RedmineCache<ProjectList.Project> _projectCache;
|
private readonly RedmineCache<ProjectList.Project> _projectCache;
|
||||||
|
private readonly RedmineCache<UserInfo.User> _userCache;
|
||||||
|
|
||||||
public RedmineManager(RedmineConfig config, HttpClient client, ILoggerFactory loggerFactory)
|
public RedmineManager(RedmineConfig config, HttpClient client, ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
@@ -31,6 +32,9 @@ namespace Blueberry.Redmine
|
|||||||
_projectCache = new RedmineCache<ProjectList.Project>(
|
_projectCache = new RedmineCache<ProjectList.Project>(
|
||||||
DEFAULT_CACHE_DURATION, loggerFactory.CreateLogger<RedmineCache<ProjectList.Project>>(), cacheFilePath: $"{_config.CacheFilePath}Projects.json");
|
DEFAULT_CACHE_DURATION, loggerFactory.CreateLogger<RedmineCache<ProjectList.Project>>(), cacheFilePath: $"{_config.CacheFilePath}Projects.json");
|
||||||
|
|
||||||
|
_userCache = new RedmineCache<UserInfo.User>(
|
||||||
|
DEFAULT_CACHE_DURATION, loggerFactory.CreateLogger<RedmineCache<UserInfo.User>>(), cacheFilePath: $"{_config.CacheFilePath}Users.json");
|
||||||
|
|
||||||
_logger = loggerFactory.CreateLogger<RedmineManager>();
|
_logger = loggerFactory.CreateLogger<RedmineManager>();
|
||||||
_logger.LogDebug("Initialized caches");
|
_logger.LogDebug("Initialized caches");
|
||||||
}
|
}
|
||||||
@@ -87,11 +91,22 @@ namespace Blueberry.Redmine
|
|||||||
{
|
{
|
||||||
return await _projectCache.GetItemsAsync();
|
return await _projectCache.GetItemsAsync();
|
||||||
}
|
}
|
||||||
var projects = await _apiClient.GetProjects(limit, progress, token);
|
var projects = await _apiClient.GetProjectsAsync(limit, progress, token);
|
||||||
await _projectCache.RefreshCacheAsync(projects);
|
await _projectCache.RefreshCacheAsync(projects);
|
||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<UserInfo.User>> GetUsersAsync(int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||||
|
{
|
||||||
|
if (_userCache.IsCacheValid())
|
||||||
|
{
|
||||||
|
return await _userCache.GetItemsAsync();
|
||||||
|
}
|
||||||
|
var users = await _apiClient.GetUsersAsync(limit, progress, token);
|
||||||
|
await _userCache.RefreshCacheAsync(users);
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<UserInfo.User> GetCurrentUserAsync(CancellationToken? token = null)
|
public async Task<UserInfo.User> GetCurrentUserAsync(CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
var user = await _apiClient.GetUserAsync(token: token);
|
var user = await _apiClient.GetUserAsync(token: token);
|
||||||
@@ -104,42 +119,74 @@ namespace Blueberry.Redmine
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<IssueList.Issue>> GetCurrentUserIssuesAsync(int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
public async Task<List<IssueList.Issue>> GetCurrentUserOpenIssuesAsync(int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
var user = await GetCurrentUserAsync(token);
|
var user = await GetCurrentUserAsync(token);
|
||||||
return await _apiClient.GetOpenIssuesByAssignee(user.Id, limit, progress, token);
|
return await _apiClient.GetOpenIssuesByAssigneeAsync(user.Id, limit, progress, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<IssueList.Issue>> GetUserOpenIssuesAsync(int userId, int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||||
|
{
|
||||||
|
return await _apiClient.GetOpenIssuesByAssigneeAsync(userId, limit, progress, 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 = 50,
|
||||||
|
IProgress<(int, int)>? progress = null,
|
||||||
|
CancellationToken? token = null)
|
||||||
|
{
|
||||||
|
return await _apiClient.GetIssuesAsync(userId, projectId, statusId, isOpen, createdFrom, createdTo, updatedFrom, updatedTo, limit, progress, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddComment(int issueId, string comment, bool isPrivate = false, CancellationToken? token = null)
|
||||||
|
{
|
||||||
|
await _apiClient.AddCommentToIssueAsync(issueId, comment, isPrivate, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<double> GetCurrentUserTimeAsync(DateTime start, DateTime end, int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
public async Task<double> GetCurrentUserTimeAsync(DateTime start, DateTime end, int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
var user = await GetCurrentUserAsync(token);
|
var user = await GetCurrentUserAsync(token);
|
||||||
return await _apiClient.GetTotalTimeForUser(user.Id, start, end, limit, progress, token);
|
return await _apiClient.GetTotalTimeForUserAsync(user.Id, start, end, limit, progress, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<double> GetUserTimeAsync(int userId, DateTime start, DateTime end, int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||||
|
{
|
||||||
|
return await _apiClient.GetTotalTimeForUserAsync(userId, start, end, limit, progress, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DetailedIssue.Issue> GetIssueAsync(int issueId, CancellationToken? token = null)
|
public async Task<DetailedIssue.Issue> GetIssueAsync(int issueId, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
return await _apiClient.GetIssue(issueId, token);
|
return await _apiClient.GetIssueAsync(issueId, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IssueList.Issue> GetSimpleIssueAsync(int issueId, CancellationToken? token = null)
|
public async Task<IssueList.Issue> GetSimpleIssueAsync(int issueId, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
return await _apiClient.GetSimpleIssue(issueId, token);
|
return await _apiClient.GetSimpleIssueAsync(issueId, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<ProjectTrackers.Tracker>> GetProjectTrackersAsync(int projectId, CancellationToken? token = null)
|
public async Task<List<ProjectTrackers.Tracker>> GetProjectTrackersAsync(int projectId, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
return await _apiClient.GetTrackersForProject(projectId.ToString(), token);
|
return await _apiClient.GetTrackersForProjectAsync(projectId.ToString(), token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<TimeOnIssue.TimeEntry>> GetTimeOnIssue(int issueId, int limit = 25, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
public async Task<List<TimeOnIssue.TimeEntry>> GetTimeOnIssue(int issueId, int limit = 25, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
var result = await _apiClient.GetTimeOnIssue(issueId, limit, progress, token);
|
var result = await _apiClient.GetTimeOnIssueAsync(issueId, limit, progress, token);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetIssueStatusAsync(int issueId, int statusId, CancellationToken? token = null)
|
public async Task SetIssueStatusAsync(int issueId, int statusId, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
await _apiClient.SetIssueStatus(issueId, statusId, token);
|
await _apiClient.SetIssueStatusAsync(issueId, statusId, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LogTimeAsync(int issueId, double hours, string comments, DateTime? date = null, int? activityId = null, CancellationToken? token = null)
|
public async Task LogTimeAsync(int issueId, double hours, string comments, DateTime? date = null, int? activityId = null, CancellationToken? token = null)
|
||||||
@@ -150,7 +197,7 @@ namespace Blueberry.Redmine
|
|||||||
public async Task<int> CreateIssueAsync(int projectId, int trackerId, string subject, string description,
|
public async Task<int> CreateIssueAsync(int projectId, int trackerId, string subject, string description,
|
||||||
double estimatedHours, int priorityId, int? assigneeId = null, int? parentIssueId = null, CancellationToken? token = null)
|
double estimatedHours, int priorityId, int? assigneeId = null, int? parentIssueId = null, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
return await _apiClient.CreateNewIssue(projectId, trackerId, subject, description, estimatedHours, priorityId, assigneeId, parentIssueId, token);
|
return await _apiClient.CreateNewIssueAsync(projectId, trackerId, subject, description, estimatedHours, priorityId, assigneeId, parentIssueId, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<double> GetCurrentUserTimeTodayAsync(int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
public async Task<double> GetCurrentUserTimeTodayAsync(int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||||
@@ -169,5 +216,10 @@ namespace Blueberry.Redmine
|
|||||||
var end = start.AddMonths(1).AddDays(-1);
|
var end = start.AddMonths(1).AddDays(-1);
|
||||||
return await GetCurrentUserTimeAsync(start, end, limit, progress, token);
|
return await GetCurrentUserTimeAsync(start, end, limit, progress, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<UserTime.TimeEntry>> GetTimeForUserAsync(int userId, DateTime start, DateTime end, int limit = 50, IProgress<(int, int)>? progress = null, CancellationToken? token = null)
|
||||||
|
{
|
||||||
|
return await _apiClient.GetTimeForUserAsync(userId, start, end, limit, progress, token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
|
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|||||||
133
Blueberry/HoursWindow.xaml
Normal file
133
Blueberry/HoursWindow.xaml
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<ui:FluentWindow x:Class="Blueberry.HoursWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||||
|
xmlns:local="clr-namespace:Blueberry"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DataContext="{d:DesignInstance local:HoursWindow}"
|
||||||
|
Loaded="FluentWindow_Loaded"
|
||||||
|
Title="HoursWindow" Height="550" Width="800" MinWidth="670" MinHeight="450">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="1*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="1*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ui:TitleBar Title="Órák" Grid.ColumnSpan="2">
|
||||||
|
<ui:TitleBar.Icon>
|
||||||
|
<ui:ImageIcon Source="/bb.ico" />
|
||||||
|
</ui:TitleBar.Icon>
|
||||||
|
</ui:TitleBar>
|
||||||
|
<Grid x:Name="userSelectionGrid" Grid.Row="1" Grid.ColumnSpan="2">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="1*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ui:TextBlock Text="Felhasználó" VerticalAlignment="Center" Margin="10" />
|
||||||
|
<ComboBox x:Name="userComboBox" Grid.Column="1" Margin="10, 10, 5, 10" IsEditable="True" DisplayMemberPath="FullName" />
|
||||||
|
<ui:Button Grid.Column="2" Content="Dátum" Margin="5, 10" Padding="10" x:Name="dateButton" Click="dateButton_Click">
|
||||||
|
<ui:Button.Icon>
|
||||||
|
<ui:SymbolIcon Symbol="Timeline24" />
|
||||||
|
</ui:Button.Icon>
|
||||||
|
</ui:Button>
|
||||||
|
<ui:Button Grid.Column="3" Content="Keresés" Margin="5, 10, 10, 10" Padding="10" x:Name="searchButton" Click="searchButton_Click">
|
||||||
|
<ui:Button.Icon>
|
||||||
|
<ui:SymbolIcon Symbol="Search24" />
|
||||||
|
</ui:Button.Icon>
|
||||||
|
</ui:Button>
|
||||||
|
<ui:Flyout Grid.Column="2" x:Name="calendarFlyout">
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<Calendar x:Name="userCalendar" SelectionMode="SingleRange" IsTodayHighlighted="True" />
|
||||||
|
</StackPanel>
|
||||||
|
</ui:Flyout>
|
||||||
|
</Grid>
|
||||||
|
<ui:ProgressRing x:Name="hoursProgress" Grid.Row="2" Height="50" Width="50" IsIndeterminate="True" />
|
||||||
|
<ui:Card Grid.Row="2" Margin="10, 10, 0, 10" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
|
||||||
|
<ui:ListView ItemsSource="{Binding Hours}" Grid.IsSharedSizeScope="True" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
|
VirtualizingPanel.ScrollUnit="Pixel">
|
||||||
|
<ui:ListView.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" SharedSizeGroup="x" />
|
||||||
|
<ColumnDefinition Width="1*" />
|
||||||
|
<ColumnDefinition Width="2*" />
|
||||||
|
<ColumnDefinition Width="Auto" SharedSizeGroup="y" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ui:Card Visibility="{Binding CardVisibility}" Padding="50, 2, 50, 2" Margin="50, 2, 50, 2" Grid.ColumnSpan="4" HorizontalAlignment="Center">
|
||||||
|
<ui:TextBlock Text="{Binding SeparatorText}" FontStyle="Italic" FontWeight="Bold" Margin="0, 6, 0, 0"
|
||||||
|
FontSize="10" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</ui:Card>
|
||||||
|
<ui:Button Grid.Column="0" Margin="5" Content="{Binding IssueId}" x:Name="openIssueButton" Click="openIssueButton_Click" Visibility="{Binding ButtonVisibility}"
|
||||||
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
|
||||||
|
<Grid Grid.Column="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="2*" />
|
||||||
|
<RowDefinition Height="1*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<ui:TextBlock Grid.Row="0" Margin="5, 2, 5, 0" Text="{Binding IssueName}" TextTrimming="CharacterEllipsis" />
|
||||||
|
<ui:TextBlock Grid.Row="1" Margin="5, 0, 5, 2" Text="{Binding ProjectName}" FontSize="10" TextTrimming="CharacterEllipsis" />
|
||||||
|
</Grid>
|
||||||
|
<ui:TextBlock Grid.Column="2" Margin="5" Text="{Binding Comments}" ToolTip="{Binding Comments}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||||
|
<ui:TextBlock Grid.Column="3" Margin="5, 5, 10, 5" Text="{Binding Hours}" FontSize="18" VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ui:ListView.ItemTemplate>
|
||||||
|
</ui:ListView>
|
||||||
|
</ui:Card>
|
||||||
|
|
||||||
|
<ui:Card Grid.Column="1" Grid.Row="2" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="10">
|
||||||
|
<Grid x:Name="dataGrid" VerticalAlignment="Stretch">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="1*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="1*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<ui:TextBlock Grid.Row="3" Margin="0,150" />
|
||||||
|
|
||||||
|
<ui:TextBlock Grid.Column="0" Grid.Row="0" Margin="5" Text="Összes óra:" />
|
||||||
|
<ui:TextBlock Grid.Column="0" Grid.Row="1" Margin="5" Text="Átlag (per nap):" />
|
||||||
|
<ui:TextBlock Grid.Column="0" Grid.Row="2" Margin="5" Text="Átlag (per munkanap):" />
|
||||||
|
<ui:TextBlock Grid.Column="0" Grid.Row="4" Margin="5" Text="Új jegyek:" />
|
||||||
|
<ui:TextBlock Grid.Column="0" Grid.Row="5" Margin="5" Text="Lezárt jegyek:" />
|
||||||
|
<ui:TextBlock Grid.Column="0" Grid.Row="6" Margin="5" Text="Min. jegy életkor:" />
|
||||||
|
<ui:TextBlock Grid.Column="0" Grid.Row="7" Margin="5" Text="Max. jegy életkor:" />
|
||||||
|
<ui:TextBlock Grid.Column="0" Grid.Row="8" Margin="5" Text="Átlag jegy életkor:" />
|
||||||
|
<ui:TextBlock Grid.Column="0" Grid.Row="9" Margin="5" Text="Medián jegy életkor:" />
|
||||||
|
|
||||||
|
<ui:TextBlock Grid.Column="1" Grid.Row="0" Margin="5" Text="0.0" x:Name="totalHoursTextBlock" />
|
||||||
|
<ui:TextBlock Grid.Column="1" Grid.Row="1" Margin="5" Text="0.0" x:Name="avgDayTextBlock" />
|
||||||
|
<ui:TextBlock Grid.Column="1" Grid.Row="2" Margin="5" Text="0.0" x:Name="avgWorkdayTextBlock"/>
|
||||||
|
<ui:TextBlock Grid.Column="1" Grid.Row="4" Margin="5" Text="0.0" x:Name="newTicketsTextBlock" />
|
||||||
|
<ui:TextBlock Grid.Column="1" Grid.Row="5" Margin="5" Text="0.0" x:Name="closedTicketsTextBlock" />
|
||||||
|
<ui:TextBlock Grid.Column="1" Grid.Row="6" Margin="5" Text="0.0" x:Name="minTicketAgeTextBlock" />
|
||||||
|
<ui:TextBlock Grid.Column="1" Grid.Row="7" Margin="5" Text="0.0" x:Name="maxTicketAgeTextBlock" />
|
||||||
|
<ui:TextBlock Grid.Column="1" Grid.Row="8" Margin="5" Text="0.0" x:Name="avgTicketAgeTextBlock" />
|
||||||
|
<ui:TextBlock Grid.Column="1" Grid.Row="9" Margin="5" Text="0.0" x:Name="medianTicketAgeTextBlock" />
|
||||||
|
</Grid>
|
||||||
|
</ui:Card>
|
||||||
|
</Grid>
|
||||||
|
</ui:FluentWindow>
|
||||||
193
Blueberry/HoursWindow.xaml.cs
Normal file
193
Blueberry/HoursWindow.xaml.cs
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
using Blueberry.Redmine;
|
||||||
|
using Blueberry.Redmine.Dto;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using Wpf.Ui.Controls;
|
||||||
|
|
||||||
|
namespace Blueberry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for HoursWindow.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class HoursWindow : FluentWindow
|
||||||
|
{
|
||||||
|
private readonly List<UserInfo.User> _users = [];
|
||||||
|
private readonly RedmineManager _manager;
|
||||||
|
private readonly RedmineConfig _config;
|
||||||
|
private readonly ConcurrentDictionary<int, string> _issueNames = [];
|
||||||
|
|
||||||
|
public ObservableCollection<DisplayHours> Hours { get; set; } = [];
|
||||||
|
|
||||||
|
public HoursWindow(RedmineManager manager, RedmineConfig config)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = this;
|
||||||
|
_manager = manager;
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void FluentWindow_Loaded(object sender, System.Windows.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var u = await _manager.GetUsersAsync();
|
||||||
|
var current = await _manager.GetCurrentUserAsync();
|
||||||
|
hoursProgress.Visibility = Visibility.Hidden;
|
||||||
|
_users.Clear();
|
||||||
|
_users.AddRange(u);
|
||||||
|
userComboBox.Items.Clear();
|
||||||
|
foreach (var user in u)
|
||||||
|
userComboBox.Items.Add(user);
|
||||||
|
|
||||||
|
userComboBox.SelectedItem = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dateButton_Click(object sender, System.Windows.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
calendarFlyout.IsOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void searchButton_Click(object sender, System.Windows.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var user = userComboBox.SelectedItem as UserInfo.User;
|
||||||
|
if(user is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var selectedDates = userCalendar.SelectedDates;
|
||||||
|
if (selectedDates.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ConcurrentBag<DisplayHours> hours = [];
|
||||||
|
Task[] tasks = new Task[selectedDates.Count];
|
||||||
|
Hours.Clear();
|
||||||
|
hoursProgress.Visibility= Visibility.Visible;
|
||||||
|
|
||||||
|
for (int i = 0; i < selectedDates.Count; i++)
|
||||||
|
{
|
||||||
|
var date = selectedDates[i];
|
||||||
|
tasks[i] = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var h = await _manager.GetTimeForUserAsync(user.Id, date, date);
|
||||||
|
foreach (var item in h)
|
||||||
|
{
|
||||||
|
var dh = new DisplayHours()
|
||||||
|
{
|
||||||
|
ProjectName = item.Project.Name,
|
||||||
|
IssueName = await GetIssueNameAsync(item.Issue.Id),
|
||||||
|
IssueId = item.Issue.Id.ToString(),
|
||||||
|
Date = date,
|
||||||
|
CreatedOn = item.CreatedOn,
|
||||||
|
Hours = item.Hours.ToString(),
|
||||||
|
Comments = item.Comments,
|
||||||
|
IsSeparator = false
|
||||||
|
};
|
||||||
|
hours.Add(dh);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Task.Delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
var newTickets = await _manager.GetIssuesAsync(user.Id, createdFrom: selectedDates.First(), createdTo: selectedDates.Last());
|
||||||
|
var closedTickets = await _manager.GetIssuesAsync(user.Id, isOpen: false, updatedFrom: selectedDates.First(), updatedTo: selectedDates.Last());
|
||||||
|
var currentTickets = await _manager.GetUserOpenIssuesAsync(user.Id);
|
||||||
|
|
||||||
|
var total = hours.Sum(x => double.Parse(x.Hours));
|
||||||
|
totalHoursTextBlock.Text = total.ToString();
|
||||||
|
avgDayTextBlock.Text = Math.Round(total / selectedDates.Count, 2).ToString();
|
||||||
|
|
||||||
|
int workingDays = 0;
|
||||||
|
foreach (var day in selectedDates)
|
||||||
|
{
|
||||||
|
if(day.DayOfWeek != DayOfWeek.Saturday &&
|
||||||
|
day.DayOfWeek != DayOfWeek.Sunday)
|
||||||
|
workingDays++;
|
||||||
|
}
|
||||||
|
avgWorkdayTextBlock.Text = Math.Round(total / workingDays, 2).ToString();
|
||||||
|
|
||||||
|
newTicketsTextBlock.Text = newTickets.Count.ToString();
|
||||||
|
closedTicketsTextBlock.Text = closedTickets.Count.ToString();
|
||||||
|
|
||||||
|
minTicketAgeTextBlock.Text = Math.Round(currentTickets.Min(x => (DateTime.Now - x.CreatedOn).TotalDays), 2) + " nap";
|
||||||
|
maxTicketAgeTextBlock.Text = Math.Round(currentTickets.Max(x => (DateTime.Now - x.CreatedOn).TotalDays), 2) + " nap";
|
||||||
|
avgTicketAgeTextBlock.Text = Math.Round(currentTickets.Average(x => (DateTime.Now - x.CreatedOn).TotalDays), 2) + " nap";
|
||||||
|
var ages = currentTickets.Select(x => (DateTime.Now - x.CreatedOn).TotalDays).Order().ToList();
|
||||||
|
medianTicketAgeTextBlock.Text = Math.Round(ages[ages.Count / 2], 2) + " nap";
|
||||||
|
|
||||||
|
var orderedHours = hours.OrderByDescending(h => h.CreatedOn).OrderBy(h => h.Date).ToList();
|
||||||
|
var previousDate = DateTime.MinValue;
|
||||||
|
for (int i = 0; i < orderedHours.Count; i++)
|
||||||
|
{
|
||||||
|
if(orderedHours[i].Date.Date > previousDate)
|
||||||
|
{
|
||||||
|
total = orderedHours.Where(x => x.Date.Date == orderedHours[i].Date.Date).Sum(x => double.Parse(x.Hours));
|
||||||
|
var dh = new DisplayHours()
|
||||||
|
{
|
||||||
|
IsSeparator = true,
|
||||||
|
SeparatorText = orderedHours[i].Date.ToString("yyyy-MM-dd") + " | Összesen: " + total + " óra"
|
||||||
|
};
|
||||||
|
orderedHours.Insert(i, dh);
|
||||||
|
previousDate = orderedHours[i+1].Date.Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hoursProgress.Visibility = Visibility.Hidden;
|
||||||
|
|
||||||
|
foreach (var item in orderedHours)
|
||||||
|
Hours.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetIssueNameAsync(int issueId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_issueNames.ContainsKey(issueId))
|
||||||
|
return _issueNames[issueId];
|
||||||
|
|
||||||
|
var name = (await _manager.GetSimpleIssueAsync(issueId)).Subject;
|
||||||
|
_issueNames.TryAdd(issueId, name);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
Console.WriteLine(issueId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DisplayHours
|
||||||
|
{
|
||||||
|
public string ProjectName { get; set; } = "";
|
||||||
|
public string IssueName { get; set; } = "";
|
||||||
|
public string IssueId { get; set; } = "";
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public DateTime CreatedOn { get; set; }
|
||||||
|
public string Hours { get; set; } = "";
|
||||||
|
public string Comments { get; set; } = "";
|
||||||
|
public string SeparatorText { get; set; } = "";
|
||||||
|
public bool IsSeparator { get; set; }
|
||||||
|
public Visibility CardVisibility => IsSeparator ? Visibility.Visible : Visibility.Hidden;
|
||||||
|
public Visibility ButtonVisibility => IsSeparator ? Visibility.Hidden : Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void openIssueButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is FrameworkElement button && button.DataContext is DisplayHours item)
|
||||||
|
{
|
||||||
|
var issueId = int.Parse(item.IssueId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var issue = await _manager.GetIssueAsync(issueId);
|
||||||
|
var issueWindow = new IssueWindow(issue, _manager, _config);
|
||||||
|
issueWindow.Show();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DataContext="{d:DesignInstance local:IssueWindow}"
|
d:DataContext="{d:DesignInstance local:IssueWindow}"
|
||||||
Loaded="FluentWindow_Loaded"
|
Loaded="FluentWindow_Loaded"
|
||||||
|
Closing="FluentWindow_Closing"
|
||||||
Title="IssueWindow" Height="500" Width="900">
|
Title="IssueWindow" Height="500" Width="900">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using Windows.Networking.NetworkOperators;
|
||||||
using Wpf.Ui.Controls;
|
using Wpf.Ui.Controls;
|
||||||
|
|
||||||
namespace Blueberry
|
namespace Blueberry
|
||||||
@@ -17,6 +18,7 @@ namespace Blueberry
|
|||||||
private readonly RedmineManager _manager;
|
private readonly RedmineManager _manager;
|
||||||
private readonly RedmineConfig _config;
|
private readonly RedmineConfig _config;
|
||||||
private readonly List<JournalDisplay> _journalDisplays = [];
|
private readonly List<JournalDisplay> _journalDisplays = [];
|
||||||
|
private CancellationTokenSource _tokenSource = new();
|
||||||
public ObservableCollection<JournalDisplay> Journals { get; set; } = [];
|
public ObservableCollection<JournalDisplay> Journals { get; set; } = [];
|
||||||
|
|
||||||
public IssueWindow(DetailedIssue.Issue issue, RedmineManager manager, RedmineConfig config)
|
public IssueWindow(DetailedIssue.Issue issue, RedmineManager manager, RedmineConfig config)
|
||||||
@@ -37,7 +39,12 @@ namespace Blueberry
|
|||||||
iUpdatedTextBox.Text = _issue.UpdatedOn.ToString("yyyy-MM-dd");
|
iUpdatedTextBox.Text = _issue.UpdatedOn.ToString("yyyy-MM-dd");
|
||||||
iSpentTimeTextBox.Text = _issue.SpentHours.ToString();
|
iSpentTimeTextBox.Text = _issue.SpentHours.ToString();
|
||||||
journalProgressRing.Visibility = Visibility.Visible;
|
journalProgressRing.Visibility = Visibility.Visible;
|
||||||
var hours = await _manager.GetTimeOnIssue(_issue.Id);
|
|
||||||
|
List<TimeOnIssue.TimeEntry> hours = [];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
hours = await _manager.GetTimeOnIssue(_issue.Id, progress: UpdateProgress(), token: _tokenSource.Token);
|
||||||
|
} catch { }
|
||||||
_journalDisplays.AddRange(await ProcessJournal(_issue.Journals, hours));
|
_journalDisplays.AddRange(await ProcessJournal(_issue.Journals, hours));
|
||||||
if(!_journalDisplays.Any(x=>!x.IsData))
|
if(!_journalDisplays.Any(x=>!x.IsData))
|
||||||
detailsToggleSwitch.IsChecked = true;
|
detailsToggleSwitch.IsChecked = true;
|
||||||
@@ -45,6 +52,20 @@ namespace Blueberry
|
|||||||
journalProgressRing.Visibility = Visibility.Hidden;
|
journalProgressRing.Visibility = Visibility.Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IProgress<(int, int)> UpdateProgress()
|
||||||
|
{
|
||||||
|
var p = new Progress<(int current, int total)>((x) =>
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
journalProgressRing.IsIndeterminate = false;
|
||||||
|
var percent = (int)Math.Round((double)x.current / x.total * 100);
|
||||||
|
journalProgressRing.Progress = percent;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadJournal()
|
private async Task LoadJournal()
|
||||||
{
|
{
|
||||||
var showDetails = detailsToggleSwitch.IsChecked ?? true;
|
var showDetails = detailsToggleSwitch.IsChecked ?? true;
|
||||||
@@ -72,6 +93,11 @@ namespace Blueberry
|
|||||||
};
|
};
|
||||||
Process.Start(psi);
|
Process.Start(psi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void FluentWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||||
|
{
|
||||||
|
_tokenSource.Cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class IssueWindow
|
public partial class IssueWindow
|
||||||
|
|||||||
@@ -368,10 +368,8 @@ namespace BlueMine
|
|||||||
|
|
||||||
private async void hoursButton_Click(object sender, RoutedEventArgs e)
|
private async void hoursButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
await new Wpf.Ui.Controls.MessageBox
|
var hoursWindow = new HoursWindow(_manager, _config);
|
||||||
{
|
hoursWindow.Show();
|
||||||
Title = "Under construction"
|
|
||||||
}.ShowDialogAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateButton_Click(object sender, RoutedEventArgs e)
|
private void updateButton_Click(object sender, RoutedEventArgs e)
|
||||||
@@ -487,7 +485,7 @@ namespace BlueMine
|
|||||||
foreach (var issueId in Constants.StaticTickets)
|
foreach (var issueId in Constants.StaticTickets)
|
||||||
_issues.Add(await _manager.GetSimpleIssueAsync(issueId));
|
_issues.Add(await _manager.GetSimpleIssueAsync(issueId));
|
||||||
|
|
||||||
_issues.AddRange(await _manager.GetCurrentUserIssuesAsync(progress: UpdateProgress("Jegyek letöltése:")));
|
_issues.AddRange(await _manager.GetCurrentUserOpenIssuesAsync(progress: UpdateProgress("Jegyek letöltése:")));
|
||||||
progressBar.Value = 0;
|
progressBar.Value = 0;
|
||||||
progressRing.Visibility = Visibility.Hidden;
|
progressRing.Visibility = Visibility.Hidden;
|
||||||
FilterIssues();
|
FilterIssues();
|
||||||
@@ -528,7 +526,7 @@ namespace BlueMine
|
|||||||
statusTextBlock.Text = $"Kapcsolódás Redminehoz...";
|
statusTextBlock.Text = $"Kapcsolódás Redminehoz...";
|
||||||
|
|
||||||
int maxRetries = 3;
|
int maxRetries = 3;
|
||||||
int timeoutSeconds = 1; // Force kill after 5s
|
int timeoutSeconds = 3; // Force kill after 5s
|
||||||
|
|
||||||
for (int i = 0; i < maxRetries; i++)
|
for (int i = 0; i < maxRetries; i++)
|
||||||
{
|
{
|
||||||
@@ -555,7 +553,7 @@ namespace BlueMine
|
|||||||
statusTextBlock.Text = $"Kapcsolódási hiba. Újrapróbálkozás: {i + 1}/{maxRetries}";
|
statusTextBlock.Text = $"Kapcsolódási hiba. Újrapróbálkozás: {i + 1}/{maxRetries}";
|
||||||
|
|
||||||
// Wait 1 second before retrying
|
// Wait 1 second before retrying
|
||||||
await Task.Delay(1000);
|
await Task.Delay(5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// All attempts failed
|
// All attempts failed
|
||||||
|
|||||||
Reference in New Issue
Block a user