diff --git a/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.csproj b/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.csproj index 6d0b3c8..e7bbcc3 100644 --- a/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.csproj +++ b/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.csproj @@ -17,9 +17,6 @@ - - - PreserveNewest diff --git a/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.sln b/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.sln index 3d0754b..ba1c1f3 100644 --- a/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.sln +++ b/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 16.0.31229.75 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureDevOpsStateTracker.Functions", "AzureDevOpsStateTracker.Functions.csproj", "{3C86C085-C8C6-46BB-8315-D4348928034C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureDevopsStateTracker", "..\..\azure-devops-state-tracker\AzureDevopsStateTracker\AzureDevopsStateTracker.csproj", "{E71EE8C3-7D74-4C91-B244-2394160F7486}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -17,10 +15,6 @@ Global {3C86C085-C8C6-46BB-8315-D4348928034C}.Debug|Any CPU.Build.0 = Debug|Any CPU {3C86C085-C8C6-46BB-8315-D4348928034C}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C86C085-C8C6-46BB-8315-D4348928034C}.Release|Any CPU.Build.0 = Release|Any CPU - {E71EE8C3-7D74-4C91-B244-2394160F7486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E71EE8C3-7D74-4C91-B244-2394160F7486}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E71EE8C3-7D74-4C91-B244-2394160F7486}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E71EE8C3-7D74-4C91-B244-2394160F7486}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AzureDevopsStateTracker/Adapters/WorkItemAdapter.cs b/AzureDevopsStateTracker/Adapters/WorkItemAdapter.cs new file mode 100644 index 0000000..904f362 --- /dev/null +++ b/AzureDevopsStateTracker/Adapters/WorkItemAdapter.cs @@ -0,0 +1,109 @@ +using AzureDevopsStateTracker.DTOs; +using AzureDevopsStateTracker.Entities; +using AzureDevopsStateTracker.Interfaces; +using System.Collections.Generic; +using System.Linq; + +namespace AzureDevopsStateTracker.Adapters +{ + internal class WorkItemAdapter : IWorkItemAdapter + { + public WorkItemDTO ToWorkItemDTO(WorkItem workItem) + { + if (workItem == null) return null; + + return new WorkItemDTO() + { + Id = workItem.Id, + CreatedAt = workItem.CreatedAt, + AssignedTo = workItem.AssignedTo, + CreatedBy = workItem.CreatedBy, + CurrentStatus = workItem.CurrentStatus, + TeamProject = workItem.TeamProject, + AreaPath = workItem.AreaPath, + IterationPath = workItem.IterationPath, + Title = workItem.Title, + Type = workItem.Type, + Effort = workItem.Effort, + StoryPoints = workItem.StoryPoints, + OriginalEstimate = workItem.OriginalEstimate, + WorkItemParentId = workItem.WorkItemParentId, + Activity = workItem.Activity, + Tags = workItem.Tags == null ? new List() : workItem.Tags.Split(';').ToList(), + WorkItemsChangesDTO = ToWorkItemsChangeDTO(workItem.WorkItemsChanges.OrderBy(x => x.CreatedAt).ToList()), + TimesByStateDTO = ToTimeByStatesDTO(workItem.CalculateTotalTimeByState().ToList()), + }; + } + + public List ToWorkItemsDTO(List workItems) + { + var workItemsDTO = new List(); + + if (workItems == null) return workItemsDTO; + + workItems.ForEach( + workItem => + workItemsDTO.Add(ToWorkItemDTO(workItem))); + + return workItemsDTO + .ToList(); + } + + public WorkItemChangeDTO ToWorkItemChangeDTO(WorkItemChange workIteChange) + { + if (workIteChange == null) return null; + + return new WorkItemChangeDTO() + { + NewDate = workIteChange.NewDate, + NewState = workIteChange.NewState, + OldState = workIteChange.OldState, + OldDate = workIteChange.OldDate, + ChangedBy = workIteChange.ChangedBy + }; + } + + public List ToWorkItemsChangeDTO(List workItemsChanges) + { + var workItemsChangeDTO = new List(); + + if (workItemsChanges == null) return workItemsChangeDTO; + + workItemsChanges.ForEach( + workItemsChange => + workItemsChangeDTO.Add(ToWorkItemChangeDTO(workItemsChange))); + + return workItemsChangeDTO + .Where(w => w != null) + .ToList(); + } + + public TimeByStateDTO ToTimeByStateDTO(TimeByState workItemStatusTime) + { + if (workItemStatusTime == null) return null; + + return new TimeByStateDTO() + { + CreatedAt = workItemStatusTime.CreatedAt, + State = workItemStatusTime.State, + //TotalTime = workItemStatusTime.TotalTimeText, + //TotalWorkedTime = workItemStatusTime.TotalWorkedTimeText + }; + } + + public List ToTimeByStatesDTO(List workItemStatusTimes) + { + var workItemStatusTimeDTO = new List(); + + if (workItemStatusTimes == null) return workItemStatusTimeDTO; + + workItemStatusTimes.ForEach( + workItemStatusTime => + workItemStatusTimeDTO.Add(ToTimeByStateDTO(workItemStatusTime))); + + return workItemStatusTimeDTO + .Where(w => w != null) + .ToList(); + } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/AzureDevopsStateTracker.csproj b/AzureDevopsStateTracker/AzureDevopsStateTracker.csproj new file mode 100644 index 0000000..647ec7b --- /dev/null +++ b/AzureDevopsStateTracker/AzureDevopsStateTracker.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.1 + false + + + + + + + + \ No newline at end of file diff --git a/AzureDevopsStateTracker/AzureDevopsStateTracker.sln b/AzureDevopsStateTracker/AzureDevopsStateTracker.sln new file mode 100644 index 0000000..1a63535 --- /dev/null +++ b/AzureDevopsStateTracker/AzureDevopsStateTracker.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31402.337 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureDevopsStateTracker", "AzureDevopsStateTracker.csproj", "{E00DFF81-08B1-4A71-82C4-7CED951299BE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E00DFF81-08B1-4A71-82C4-7CED951299BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E00DFF81-08B1-4A71-82C4-7CED951299BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E00DFF81-08B1-4A71-82C4-7CED951299BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E00DFF81-08B1-4A71-82C4-7CED951299BE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {53133A2B-5357-47B5-A1C4-30953F789623} + EndGlobalSection +EndGlobal diff --git a/AzureDevopsStateTracker/Configurations/Configuration.cs b/AzureDevopsStateTracker/Configurations/Configuration.cs new file mode 100644 index 0000000..a20ca36 --- /dev/null +++ b/AzureDevopsStateTracker/Configurations/Configuration.cs @@ -0,0 +1,37 @@ +using AzureDevopsStateTracker.Adapters; +using AzureDevopsStateTracker.Data; +using AzureDevopsStateTracker.Data.Context; +using AzureDevopsStateTracker.Interfaces; +using AzureDevopsStateTracker.Interfaces.Internals; +using AzureDevopsStateTracker.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace AzureDevopsStateTracker.Configurations +{ + public static class Configuration + { + public static IServiceCollection AddAzureDevopsStateTracker(this IServiceCollection services, DataBaseConfig configurations) + { + services.AddDbContext(options => + options.UseSqlServer(DataBaseConfig.ConnectionsString)); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } + + public static IApplicationBuilder UseAzureDevopsStateTracker(this IApplicationBuilder app, IServiceProvider serviceProvider) + { + var context = serviceProvider.GetService(); + context.Database.Migrate(); + + return app; + } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/DTOs/Create/CreateWorkItemDTO.cs b/AzureDevopsStateTracker/DTOs/Create/CreateWorkItemDTO.cs new file mode 100644 index 0000000..e6282cd --- /dev/null +++ b/AzureDevopsStateTracker/DTOs/Create/CreateWorkItemDTO.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace AzureDevopsStateTracker.DTOs.Create +{ + public class CreateWorkItemDTO + { + [JsonProperty("resource")] + public Resource Resource { get; set; } + } + + public class Resource + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("fields")] + public Fields Fields { get; set; } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/DTOs/Fields.cs b/AzureDevopsStateTracker/DTOs/Fields.cs new file mode 100644 index 0000000..1351315 --- /dev/null +++ b/AzureDevopsStateTracker/DTOs/Fields.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json; +using System; + +namespace AzureDevopsStateTracker.DTOs +{ + public class Fields + { + [JsonProperty("System.AreaPath")] + public string AreaPath { get; set; } + + [JsonProperty("System.TeamProject")] + public string TeamProject { get; set; } + + [JsonProperty("System.IterationPath")] + public string IterationPath { get; set; } + + [JsonProperty("System.AssignedTo")] + public string AssignedTo { get; set; } + + [JsonProperty("System.WorkItemType")] + public string Type { get; set; } + + [JsonProperty("System.CreatedDate")] + public DateTime CreatedDate { get; set; } + + [JsonProperty("System.CreatedBy")] + public string CreatedBy { get; set; } + + [JsonProperty("System.ChangedBy")] + public string ChangedBy { get; set; } + + [JsonProperty("System.State")] + public string State { get; set; } + + [JsonProperty("System.Title")] + public string Title { get; set; } + + [JsonProperty("System.Tags")] + public string Tags { get; set; } + + [JsonProperty("System.Parent")] + public string Parent { get; set; } + + [JsonProperty("Microsoft.VSTS.Scheduling.StoryPoints")] + public string StoryPoints { get; set; } + + [JsonProperty("Microsoft.VSTS.Scheduling.OriginalEstimate")] + public string OriginalEstimate { get; set; } + + [JsonProperty("Microsoft.VSTS.Scheduling.RemainingWork")] + public string RemainingWork { get; set; } + + [JsonProperty("Microsoft.VSTS.Scheduling.Effort")] + public string Effort { get; set; } + + [JsonProperty("Microsoft.VSTS.Common.Activity")] + public string Activity { get; set; } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/DTOs/Update/UpdateWorkItemDTO.cs b/AzureDevopsStateTracker/DTOs/Update/UpdateWorkItemDTO.cs new file mode 100644 index 0000000..d0ba441 --- /dev/null +++ b/AzureDevopsStateTracker/DTOs/Update/UpdateWorkItemDTO.cs @@ -0,0 +1,68 @@ +using Newtonsoft.Json; +using System; + +namespace AzureDevopsStateTracker.DTOs.Update +{ + public class UpdatedWorkItemDTO + { + [JsonProperty("resource")] + public Resource Resource { get; set; } + } + + public class Resource + { + [JsonProperty("workItemId")] + public string WorkItemId { get; set; } + + [JsonProperty("fields")] + public ResourceFields Fields { get; set; } + + [JsonProperty("revision")] + public Revision Revision { get; set; } + } + + public class Revision + { + [JsonProperty("fields")] + public Fields Fields { get; set; } + } + + public class ResourceFields + { + [JsonProperty("System.State")] + public OldNewValues State { get; set; } + + [JsonProperty("Microsoft.VSTS.Common.StateChangeDate")] + public DateTimeOldNewValues StateChangeDate { get; set; } + + [JsonProperty("System.ChangedBy")] + public ChangedByOldNewValues ChangedBy { get; set; } + } + + public class OldNewValues + { + [JsonProperty("oldValue")] + public string OldValue { get; set; } + + [JsonProperty("newValue")] + public string NewValue { get; set; } + } + + public class DateTimeOldNewValues + { + [JsonProperty("oldValue")] + public DateTime OldValue { get; set; } + + [JsonProperty("newValue")] + public DateTime NewValue { get; set; } + } + + public class ChangedByOldNewValues + { + [JsonProperty("oldValue")] + public string OldValue { get; set; } + + [JsonProperty("newValue")] + public string NewValue { get; set; } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/DTOs/WorkItemDTO.cs b/AzureDevopsStateTracker/DTOs/WorkItemDTO.cs new file mode 100644 index 0000000..d9990a4 --- /dev/null +++ b/AzureDevopsStateTracker/DTOs/WorkItemDTO.cs @@ -0,0 +1,100 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace AzureDevopsStateTracker.DTOs +{ + public class WorkItemDTO + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + [JsonProperty("assigned_to")] + public string AssignedTo { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("effort")] + public string Effort { get; set; } + + [JsonProperty("story_points")] + public string StoryPoints { get; set; } + + [JsonProperty("original_estimate")] + public string OriginalEstimate { get; set; } + + [JsonProperty("created_by")] + public string CreatedBy { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("team_project")] + public string TeamProject { get; set; } + + [JsonProperty("iteration_path")] + public string IterationPath { get; set; } + + [JsonProperty("area_path")] + public string AreaPath { get; set; } + + [JsonProperty("current_status")] + public string CurrentStatus { get; set; } + + [JsonProperty("work_item_parent_id")] + public string WorkItemParentId { get; set; } + + [JsonProperty("activity")] + public string Activity { get; set; } + + [JsonProperty("tags")] + public IEnumerable Tags { get; set; } + + [JsonProperty("workItems_changes")] + public List WorkItemsChangesDTO { get; set; } + + [JsonProperty("times_by_state")] + public List TimesByStateDTO { get; set; } + } + + public class WorkItemChangeDTO + { + [JsonProperty("new_date")] + public DateTime NewDate { get; set; } + + [JsonProperty("new_state")] + public string NewState { get; set; } + + [JsonProperty("old_state")] + public string OldState { get; set; } + + [JsonProperty("old_date")] + public DateTime? OldDate { get; set; } + + [JsonProperty("changed_by")] + public string ChangedBy { get; set; } + } + + + public class TimeByStateDTO + { + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + [JsonProperty("updated_at")] + public DateTime UpdatedAt { get; set; } + + [JsonProperty("state")] + public string State { get; set; } + + [JsonProperty("total_time")] + public string TotalTime { get; set; } + + [JsonProperty("total_worked_time")] + public string TotalWorkedTime { get; set; } + } +} diff --git a/AzureDevopsStateTracker/Data/Context/AzureDevopsStateTrackerContext.cs b/AzureDevopsStateTracker/Data/Context/AzureDevopsStateTrackerContext.cs new file mode 100644 index 0000000..18113a6 --- /dev/null +++ b/AzureDevopsStateTracker/Data/Context/AzureDevopsStateTrackerContext.cs @@ -0,0 +1,32 @@ +using AzureDevopsStateTracker.Entities; +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; + +namespace AzureDevopsStateTracker.Data.Context +{ + public class AzureDevopsStateTrackerContext : DbContext, IDisposable + { + public AzureDevopsStateTrackerContext(DbContextOptions options) : base(options) + { } + + public DbSet WorkItems { get; set; } + public DbSet WorkItemsChange { get; set; } + public DbSet TimeByStates { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + foreach (var property in modelBuilder.Model.GetEntityTypes().SelectMany( + e => e.GetProperties().Where(p => p.ClrType == typeof(string)))) + property.SetColumnType("varchar(200)"); + + modelBuilder.HasDefaultSchema(DataBaseConfig.SchemaName); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) + optionsBuilder.UseSqlServer(DataBaseConfig.ConnectionsString); + } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Data/DataBaseConfig.cs b/AzureDevopsStateTracker/Data/DataBaseConfig.cs new file mode 100644 index 0000000..fbf1079 --- /dev/null +++ b/AzureDevopsStateTracker/Data/DataBaseConfig.cs @@ -0,0 +1,20 @@ +using AzureDevopsStateTracker.Extensions; +using System; + +namespace AzureDevopsStateTracker.Data +{ + public class DataBaseConfig + { + public DataBaseConfig(string connectionsString, string schemaName = "dbo") + { + if (connectionsString.IsNullOrEmpty()) + throw new ArgumentException("The ConnectionsString is required"); + + ConnectionsString = connectionsString; + SchemaName = schemaName; + } + + public static string ConnectionsString { get; private set; } + public static string SchemaName { get; private set; } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Data/Repository.cs b/AzureDevopsStateTracker/Data/Repository.cs new file mode 100644 index 0000000..23d4673 --- /dev/null +++ b/AzureDevopsStateTracker/Data/Repository.cs @@ -0,0 +1,70 @@ +using AzureDevopsStateTracker.Data.Context; +using AzureDevopsStateTracker.Entities; +using AzureDevopsStateTracker.Interfaces.Internals; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AzureDevopsStateTracker.Data +{ + internal abstract class Repository : IRepository where TEntity : Entity + { + protected readonly AzureDevopsStateTrackerContext Db; + protected readonly DbSet DbSet; + + public Repository(AzureDevopsStateTrackerContext db) + { + Db = db; + DbSet = db.Set(); + } + + public virtual void Add(TEntity entity) + { + DbSet.Add(entity); + } + + public virtual void Add(IEnumerable entities) + { + DbSet.AddRange(entities); + } + + public virtual void Update(TEntity entity) + { + DbSet.Update(entity); + } + + public virtual void Update(IEnumerable entities) + { + DbSet.UpdateRange(entities); + } + + public virtual void Delete(TEntity entity) + { + DbSet.Remove(entity); + } + + public virtual TEntity GetById(string id) + { + return DbSet + .Where(x => x.Id == id) + .FirstOrDefault(); + } + + public bool Exist(string id) + { + return DbSet + .Any(x => x.Id == id); + } + + public void SaveChanges() + { + Db.SaveChanges(); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Data/WorkItemRepository.cs b/AzureDevopsStateTracker/Data/WorkItemRepository.cs new file mode 100644 index 0000000..853f884 --- /dev/null +++ b/AzureDevopsStateTracker/Data/WorkItemRepository.cs @@ -0,0 +1,43 @@ +using AzureDevopsStateTracker.Data.Context; +using AzureDevopsStateTracker.Entities; +using AzureDevopsStateTracker.Interfaces.Internals; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Linq; + +namespace AzureDevopsStateTracker.Data +{ + internal class WorkItemRepository : Repository, IWorkItemRepository + { + public WorkItemRepository(AzureDevopsStateTrackerContext context) : base(context) { } + + public WorkItem GetByWorkItemId(string workItemId) + { + return DbSet + .Include(x => x.WorkItemsChanges) + .Include(x => x.TimeByStates) + .FirstOrDefault(x => x.Id == workItemId); + } + + public IEnumerable ListByWorkItemId(IEnumerable workItemsId) + { + return DbSet + .Include(x => x.WorkItemsChanges) + .Include(x => x.TimeByStates) + .Where(x => workItemsId.Contains(x.Id)); + } + + public IEnumerable ListByIterationPath(string iterationPath) + { + return DbSet + .Include(x => x.WorkItemsChanges) + .Include(x => x.TimeByStates) + .Where(x => x.IterationPath == iterationPath); + } + + public void RemoveAllTimeByState(List timeByStates) + { + Db.TimeByStates.RemoveRange(timeByStates); + } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Entities/Entity.cs b/AzureDevopsStateTracker/Entities/Entity.cs new file mode 100644 index 0000000..4f50888 --- /dev/null +++ b/AzureDevopsStateTracker/Entities/Entity.cs @@ -0,0 +1,21 @@ +using System; + +namespace AzureDevopsStateTracker.Entities +{ + public abstract class Entity + { + public string Id { get; protected set; } + public DateTime CreatedAt { get; private set; } + public Entity(string id) + { + Id = id; + CreatedAt = DateTime.UtcNow; + } + + public Entity() + { + Id = Guid.NewGuid().ToString(); + CreatedAt = DateTime.UtcNow; + } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Entities/TimeByState.cs b/AzureDevopsStateTracker/Entities/TimeByState.cs new file mode 100644 index 0000000..4145ebb --- /dev/null +++ b/AzureDevopsStateTracker/Entities/TimeByState.cs @@ -0,0 +1,36 @@ +using AzureDevopsStateTracker.Extensions; +using System; + +namespace AzureDevopsStateTracker.Entities +{ + public class TimeByState : Entity + { + public string WorkItemId { get; private set; } + public string State { get; private set; } + public double TotalTime { get; private set; } + public double TotalWorkedTime { get; private set; } + + public WorkItem WorkItem { get; private set; } + + private TimeByState() { } + + public TimeByState(string workItemId, string state, TimeSpan totalTime, TimeSpan totalWorkedTime) + { + WorkItemId = workItemId; + State = state; + TotalTime = totalTime.TotalSeconds; + TotalWorkedTime = totalWorkedTime.TotalSeconds; + + Validate(); + } + + public void Validate() + { + if (WorkItemId.IsNullOrEmpty() || Convert.ToInt64(WorkItemId) <= 0) + throw new Exception("WorkItemId is required"); + + if (State.IsNullOrEmpty()) + throw new Exception("State is required"); + } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Entities/WorkItem.cs b/AzureDevopsStateTracker/Entities/WorkItem.cs new file mode 100644 index 0000000..64aa79c --- /dev/null +++ b/AzureDevopsStateTracker/Entities/WorkItem.cs @@ -0,0 +1,128 @@ +using AzureDevopsStateTracker.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AzureDevopsStateTracker.Entities +{ + public class WorkItem : Entity + { + public string AreaPath { get; private set; } + public string TeamProject { get; private set; } + public string IterationPath { get; private set; } + public string AssignedTo { get; private set; } + public string Type { get; private set; } + public string CreatedBy { get; private set; } + public string Title { get; private set; } + public string Tags { get; private set; } + public string Effort { get; private set; } + public string OriginalEstimate { get; private set; } + public string StoryPoints { get; private set; } + public string WorkItemParentId { get; private set; } + public string Activity { get; private set; } + + private readonly List _workItemsChanges; + public IReadOnlyCollection WorkItemsChanges => _workItemsChanges; + + private readonly List _timeByState; + public IReadOnlyCollection TimeByStates => _timeByState; + public string CurrentStatus => _workItemsChanges?.OrderBy(x => x.CreatedAt)?.LastOrDefault()?.NewState; + + private WorkItem() + { + _workItemsChanges = new List(); + _timeByState = new List(); + } + + public WorkItem(string workItemId) : base(workItemId) + { + _workItemsChanges = new List(); + _timeByState = new List(); + Validate(); + } + + public void Update(string title, + string teamProject, string areaPath, + string iterationPath, string type, + string createdBy, string assignedTo, + string tags, + string workItemParentId, + string effort, + string storyPoint, + string originalEstimate, + string activity) + { + TeamProject = teamProject; + AreaPath = areaPath; + IterationPath = iterationPath; + Type = type; + Title = title; + CreatedBy = createdBy; + AssignedTo = assignedTo; + Tags = tags; + WorkItemParentId = workItemParentId; + Effort = effort; + StoryPoints = storyPoint; + OriginalEstimate = originalEstimate; + Activity = activity; + } + + public void Validate() + { + if (Id.IsNullOrEmpty()) + throw new Exception("WorkItemId is required"); + } + + public void AddWorkItemChange(WorkItemChange workItemChange) + { + if (workItemChange == null) + throw new Exception("WorkItemChange is null"); + + _workItemsChanges.Add(workItemChange); + } + + public void AddTimeByState(TimeByState timeByState) + { + if (timeByState == null) + throw new Exception("TimeByState is null"); + + _timeByState.Add(timeByState); + } + + public void AddTimesByState(IEnumerable timesByState) + { + if (!timesByState.Any()) + return; + + foreach (var timeByState in timesByState) + AddTimeByState(timeByState); + } + + public void ClearTimesByState() + { + _timeByState.Clear(); + } + + public IEnumerable CalculateTotalTimeByState() + { + var timesByStateList = new List(); + if (!_workItemsChanges.Any()) + return timesByStateList; + + foreach (var workItemChange in _workItemsChanges.OrderBy(x => x.CreatedAt).GroupBy(x => x.OldState).Where(x => x.Key != null)) + { + var totalTime = TimeSpan.Zero; + var totalWorkedTime = TimeSpan.Zero; + foreach (var data in workItemChange) + { + totalTime += data.CalculateTotalTime(); + totalWorkedTime += data.CalculateTotalWorkedTime(); + } + + timesByStateList.Add(new TimeByState(Id, workItemChange.Key, totalTime, totalWorkedTime)); + } + + return timesByStateList; + } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Entities/WorkItemChange.cs b/AzureDevopsStateTracker/Entities/WorkItemChange.cs new file mode 100644 index 0000000..b7baac5 --- /dev/null +++ b/AzureDevopsStateTracker/Entities/WorkItemChange.cs @@ -0,0 +1,59 @@ +using AzureDevopsStateTracker.Extensions; +using System; + +namespace AzureDevopsStateTracker.Entities +{ + public class WorkItemChange : Entity + { + public string WorkItemId { get; private set; } + public DateTime NewDate { get; private set; } + public DateTime? OldDate { get; private set; } + public string NewState { get; private set; } + public string OldState { get; private set; } + public string ChangedBy { get; private set; } + public WorkItem WorkItem { get; private set; } + + private WorkItemChange() { } + + public WorkItemChange(string workItemId, string changedBy, DateTime newDate, string newState, string oldState, DateTime? oldDate) + { + WorkItemId = workItemId; + NewDate = newDate; + OldDate = oldDate; + NewState = newState; + OldState = oldState; + ChangedBy = changedBy; + + Validate(); + } + + public void Validate() + { + if (WorkItemId.IsNullOrEmpty() || Convert.ToInt64(WorkItemId) <= 0) + throw new Exception("WorkItemId is required"); + } + + public TimeSpan CalculateTotalTime() + { + return OldDate == null ? TimeSpan.Zero : NewDate - OldDate.GetValueOrDefault(); + } + + public TimeSpan CalculateTotalWorkedTime() + { + if (OldDate.GetValueOrDefault() == DateTime.MinValue) + return TimeSpan.Zero; + + TimeSpan hoursWorked = TimeSpan.Zero; + for (var i = OldDate.GetValueOrDefault(); i <= NewDate; i = i.AddHours(1)) + { + if (i.DayOfWeek != DayOfWeek.Saturday && i.DayOfWeek != DayOfWeek.Sunday) + { + if ((i.TimeOfDay.Hours >= 8 && i.TimeOfDay.Hours < 12) || (i.TimeOfDay.Hours >= 14 && i.TimeOfDay.Hours < 18)) + hoursWorked += (NewDate.TimeOfDay - i.TimeOfDay); + } + } + + return hoursWorked; + } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Extensions/HelperExtenions.cs b/AzureDevopsStateTracker/Extensions/HelperExtenions.cs new file mode 100644 index 0000000..286b592 --- /dev/null +++ b/AzureDevopsStateTracker/Extensions/HelperExtenions.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; + +namespace AzureDevopsStateTracker.Extensions +{ + public static class HelperExtenions + { + public static bool IsNullOrEmpty(this string text) + { + return string.IsNullOrEmpty(text?.Trim()); + } + + public static string ExtractEmail(this string user) + { + if (user is null) + return user; + + if (!user.Contains(" <") && !user.TrimEnd().Contains(">")) + return user; + + return user.Split("<").LastOrDefault().Split(">").FirstOrDefault(); + } + + public static string ToTextTime(this TimeSpan timeSpan) + { + if (timeSpan.Days > 0) + return $@"{timeSpan:%d} Dia(s) {timeSpan:%h} h e {timeSpan:%m} min e {timeSpan:%s} s"; + + if (timeSpan.Hours > 0) + return $@"{timeSpan:%h} h e {timeSpan:%m} min e {timeSpan:%s} s"; + + if (timeSpan.Minutes > 0) + return $@"{timeSpan:%m} min e {timeSpan:%s} s"; + + if (timeSpan.Seconds > 0) + return $@"{timeSpan:%s} s"; + + return "-"; + } + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Interfaces/IAzureDevopsStateTrackerService.cs b/AzureDevopsStateTracker/Interfaces/IAzureDevopsStateTrackerService.cs new file mode 100644 index 0000000..a0ef0f9 --- /dev/null +++ b/AzureDevopsStateTracker/Interfaces/IAzureDevopsStateTrackerService.cs @@ -0,0 +1,13 @@ +using AzureDevopsStateTracker.DTOs; +using AzureDevopsStateTracker.DTOs.Create; +using AzureDevopsStateTracker.DTOs.Update; + +namespace AzureDevopsStateTracker.Interfaces +{ + public interface IAzureDevopsStateTrackerService + { + void Create(CreateWorkItemDTO createDto); + void Update(UpdatedWorkItemDTO updateDto); + WorkItemDTO GetByWorkItemId(string workItemId); + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Interfaces/IWorkItemAdapter.cs b/AzureDevopsStateTracker/Interfaces/IWorkItemAdapter.cs new file mode 100644 index 0000000..5fb5db8 --- /dev/null +++ b/AzureDevopsStateTracker/Interfaces/IWorkItemAdapter.cs @@ -0,0 +1,12 @@ +using AzureDevopsStateTracker.DTOs; +using AzureDevopsStateTracker.Entities; +using System.Collections.Generic; + +namespace AzureDevopsStateTracker.Interfaces +{ + public interface IWorkItemAdapter + { + WorkItemDTO ToWorkItemDTO(WorkItem workItem); + List ToWorkItemsDTO(List workItems); + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Interfaces/Internals/IRepository.cs b/AzureDevopsStateTracker/Interfaces/Internals/IRepository.cs new file mode 100644 index 0000000..adbfb76 --- /dev/null +++ b/AzureDevopsStateTracker/Interfaces/Internals/IRepository.cs @@ -0,0 +1,18 @@ +using AzureDevopsStateTracker.Entities; +using System; +using System.Collections.Generic; + +namespace AzureDevopsStateTracker.Interfaces.Internals +{ + public interface IRepository : IDisposable where TEntity : Entity + { + void Add(TEntity entity); + void Add(IEnumerable entities); + TEntity GetById(string id); + bool Exist(string id); + void Update(TEntity entity); + void Update(IEnumerable entities); + void Delete(TEntity entity); + void SaveChanges(); + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Interfaces/Internals/IWorkItemRepository.cs b/AzureDevopsStateTracker/Interfaces/Internals/IWorkItemRepository.cs new file mode 100644 index 0000000..82e93b9 --- /dev/null +++ b/AzureDevopsStateTracker/Interfaces/Internals/IWorkItemRepository.cs @@ -0,0 +1,13 @@ +using AzureDevopsStateTracker.Entities; +using System.Collections.Generic; + +namespace AzureDevopsStateTracker.Interfaces.Internals +{ + public interface IWorkItemRepository : IRepository + { + WorkItem GetByWorkItemId(string workItemId); + IEnumerable ListByWorkItemId(IEnumerable workItemsId); + IEnumerable ListByIterationPath(string iterationPath); + void RemoveAllTimeByState(List timeByStates); + } +} \ No newline at end of file diff --git a/AzureDevopsStateTracker/Migrations/20210719163846_AzureDevopsStateTrackerInitial.Designer.cs b/AzureDevopsStateTracker/Migrations/20210719163846_AzureDevopsStateTrackerInitial.Designer.cs new file mode 100644 index 0000000..b7fa1cf --- /dev/null +++ b/AzureDevopsStateTracker/Migrations/20210719163846_AzureDevopsStateTrackerInitial.Designer.cs @@ -0,0 +1,154 @@ +// +using System; +using AzureDevopsStateTracker.Data; +using AzureDevopsStateTracker.Data.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace AzureDevopsStateTracker.Migrations +{ + [DbContext(typeof(AzureDevopsStateTrackerContext))] + [Migration("20210719163846_AzureDevopsStateTrackerInitial")] + partial class AzureDevopsStateTrackerInitial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema(DataBaseConfig.SchemaName) + .HasAnnotation("ProductVersion", "3.1.16") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("AzureDevopsStateTracker.Entities.TimeByState", b => + { + b.Property("Id") + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("varchar(200)"); + + b.Property("TotalTime") + .HasColumnType("float"); + + b.Property("TotalWorkedTime") + .HasColumnType("float"); + + b.Property("WorkItemId") + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("WorkItemId"); + + b.ToTable("TimeByStates"); + }); + + modelBuilder.Entity("AzureDevopsStateTracker.Entities.WorkItem", b => + { + b.Property("Id") + .HasColumnType("varchar(200)"); + + b.Property("Activity") + .HasColumnType("varchar(200)"); + + b.Property("AreaPath") + .HasColumnType("varchar(200)"); + + b.Property("AssignedTo") + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("varchar(200)"); + + b.Property("Effort") + .HasColumnType("varchar(200)"); + + b.Property("IterationPath") + .HasColumnType("varchar(200)"); + + b.Property("OriginalEstimate") + .HasColumnType("varchar(200)"); + + b.Property("StoryPoints") + .HasColumnType("varchar(200)"); + + b.Property("Tags") + .HasColumnType("varchar(200)"); + + b.Property("TeamProject") + .HasColumnType("varchar(200)"); + + b.Property("Title") + .HasColumnType("varchar(200)"); + + b.Property("Type") + .HasColumnType("varchar(200)"); + + b.Property("WorkItemParentId") + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("WorkItems"); + }); + + modelBuilder.Entity("AzureDevopsStateTracker.Entities.WorkItemChange", b => + { + b.Property("Id") + .HasColumnType("varchar(200)"); + + b.Property("ChangedBy") + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("NewDate") + .HasColumnType("datetime2"); + + b.Property("NewState") + .HasColumnType("varchar(200)"); + + b.Property("OldDate") + .HasColumnType("datetime2"); + + b.Property("OldState") + .HasColumnType("varchar(200)"); + + b.Property("WorkItemId") + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("WorkItemId"); + + b.ToTable("WorkItemsChange"); + }); + + modelBuilder.Entity("AzureDevopsStateTracker.Entities.TimeByState", b => + { + b.HasOne("AzureDevopsStateTracker.Entities.WorkItem", "WorkItem") + .WithMany("TimeByStates") + .HasForeignKey("WorkItemId"); + }); + + modelBuilder.Entity("AzureDevopsStateTracker.Entities.WorkItemChange", b => + { + b.HasOne("AzureDevopsStateTracker.Entities.WorkItem", "WorkItem") + .WithMany("WorkItemsChanges") + .HasForeignKey("WorkItemId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AzureDevopsStateTracker/Migrations/20210719163846_AzureDevopsStateTrackerInitial.cs b/AzureDevopsStateTracker/Migrations/20210719163846_AzureDevopsStateTrackerInitial.cs new file mode 100644 index 0000000..de24b1a --- /dev/null +++ b/AzureDevopsStateTracker/Migrations/20210719163846_AzureDevopsStateTrackerInitial.cs @@ -0,0 +1,118 @@ +using System; +using AzureDevopsStateTracker.Data; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace AzureDevopsStateTracker.Migrations +{ + public partial class AzureDevopsStateTrackerInitial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: DataBaseConfig.SchemaName); + + migrationBuilder.CreateTable( + name: "WorkItems", + schema: DataBaseConfig.SchemaName, + columns: table => new + { + Id = table.Column(type: "varchar(200)", nullable: false), + CreatedAt = table.Column(nullable: false), + AreaPath = table.Column(type: "varchar(200)", nullable: true), + TeamProject = table.Column(type: "varchar(200)", nullable: true), + IterationPath = table.Column(type: "varchar(200)", nullable: true), + AssignedTo = table.Column(type: "varchar(200)", nullable: true), + Type = table.Column(type: "varchar(200)", nullable: true), + CreatedBy = table.Column(type: "varchar(200)", nullable: true), + Title = table.Column(type: "varchar(200)", nullable: true), + Tags = table.Column(type: "varchar(200)", nullable: true), + Effort = table.Column(type: "varchar(200)", nullable: true), + OriginalEstimate = table.Column(type: "varchar(200)", nullable: true), + StoryPoints = table.Column(type: "varchar(200)", nullable: true), + WorkItemParentId = table.Column(type: "varchar(200)", nullable: true), + Activity = table.Column(type: "varchar(200)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WorkItems", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TimeByStates", + schema: DataBaseConfig.SchemaName, + columns: table => new + { + Id = table.Column(type: "varchar(200)", nullable: false), + CreatedAt = table.Column(nullable: false), + WorkItemId = table.Column(type: "varchar(200)", nullable: true), + State = table.Column(type: "varchar(200)", nullable: true), + TotalTime = table.Column(nullable: false), + TotalWorkedTime = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TimeByStates", x => x.Id); + table.ForeignKey( + name: "FK_TimeByStates_WorkItems_WorkItemId", + column: x => x.WorkItemId, + principalSchema: DataBaseConfig.SchemaName, + principalTable: "WorkItems", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "WorkItemsChange", + schema: DataBaseConfig.SchemaName, + columns: table => new + { + Id = table.Column(type: "varchar(200)", nullable: false), + CreatedAt = table.Column(nullable: false), + WorkItemId = table.Column(type: "varchar(200)", nullable: true), + NewDate = table.Column(nullable: false), + OldDate = table.Column(nullable: true), + NewState = table.Column(type: "varchar(200)", nullable: true), + OldState = table.Column(type: "varchar(200)", nullable: true), + ChangedBy = table.Column(type: "varchar(200)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WorkItemsChange", x => x.Id); + table.ForeignKey( + name: "FK_WorkItemsChange_WorkItems_WorkItemId", + column: x => x.WorkItemId, + principalSchema: DataBaseConfig.SchemaName, + principalTable: "WorkItems", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_TimeByStates_WorkItemId", + schema: DataBaseConfig.SchemaName, + table: "TimeByStates", + column: "WorkItemId"); + + migrationBuilder.CreateIndex( + name: "IX_WorkItemsChange_WorkItemId", + schema: DataBaseConfig.SchemaName, + table: "WorkItemsChange", + column: "WorkItemId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TimeByStates", + schema: DataBaseConfig.SchemaName); + + migrationBuilder.DropTable( + name: "WorkItemsChange", + schema: DataBaseConfig.SchemaName); + + migrationBuilder.DropTable( + name: "WorkItems", + schema: DataBaseConfig.SchemaName); + } + } +} diff --git a/AzureDevopsStateTracker/Migrations/AzureDevopsStateTrackerContextModelSnapshot.cs b/AzureDevopsStateTracker/Migrations/AzureDevopsStateTrackerContextModelSnapshot.cs new file mode 100644 index 0000000..7baa9b1 --- /dev/null +++ b/AzureDevopsStateTracker/Migrations/AzureDevopsStateTrackerContextModelSnapshot.cs @@ -0,0 +1,152 @@ +// +using System; +using AzureDevopsStateTracker.Data; +using AzureDevopsStateTracker.Data.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace AzureDevopsStateTracker.Migrations +{ + [DbContext(typeof(AzureDevopsStateTrackerContext))] + partial class AzureDevopsStateTrackerContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema(DataBaseConfig.SchemaName) + .HasAnnotation("ProductVersion", "3.1.16") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("AzureDevopsStateTracker.Entities.TimeByState", b => + { + b.Property("Id") + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("varchar(200)"); + + b.Property("TotalTime") + .HasColumnType("float"); + + b.Property("TotalWorkedTime") + .HasColumnType("float"); + + b.Property("WorkItemId") + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("WorkItemId"); + + b.ToTable("TimeByStates"); + }); + + modelBuilder.Entity("AzureDevopsStateTracker.Entities.WorkItem", b => + { + b.Property("Id") + .HasColumnType("varchar(200)"); + + b.Property("Activity") + .HasColumnType("varchar(200)"); + + b.Property("AreaPath") + .HasColumnType("varchar(200)"); + + b.Property("AssignedTo") + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("varchar(200)"); + + b.Property("Effort") + .HasColumnType("varchar(200)"); + + b.Property("IterationPath") + .HasColumnType("varchar(200)"); + + b.Property("OriginalEstimate") + .HasColumnType("varchar(200)"); + + b.Property("StoryPoints") + .HasColumnType("varchar(200)"); + + b.Property("Tags") + .HasColumnType("varchar(200)"); + + b.Property("TeamProject") + .HasColumnType("varchar(200)"); + + b.Property("Title") + .HasColumnType("varchar(200)"); + + b.Property("Type") + .HasColumnType("varchar(200)"); + + b.Property("WorkItemParentId") + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("WorkItems"); + }); + + modelBuilder.Entity("AzureDevopsStateTracker.Entities.WorkItemChange", b => + { + b.Property("Id") + .HasColumnType("varchar(200)"); + + b.Property("ChangedBy") + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("NewDate") + .HasColumnType("datetime2"); + + b.Property("NewState") + .HasColumnType("varchar(200)"); + + b.Property("OldDate") + .HasColumnType("datetime2"); + + b.Property("OldState") + .HasColumnType("varchar(200)"); + + b.Property("WorkItemId") + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("WorkItemId"); + + b.ToTable("WorkItemsChange"); + }); + + modelBuilder.Entity("AzureDevopsStateTracker.Entities.TimeByState", b => + { + b.HasOne("AzureDevopsStateTracker.Entities.WorkItem", "WorkItem") + .WithMany("TimeByStates") + .HasForeignKey("WorkItemId"); + }); + + modelBuilder.Entity("AzureDevopsStateTracker.Entities.WorkItemChange", b => + { + b.HasOne("AzureDevopsStateTracker.Entities.WorkItem", "WorkItem") + .WithMany("WorkItemsChanges") + .HasForeignKey("WorkItemId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AzureDevopsStateTracker/Services/AzureDevopsStateTrackerService.cs b/AzureDevopsStateTracker/Services/AzureDevopsStateTrackerService.cs new file mode 100644 index 0000000..7b1f699 --- /dev/null +++ b/AzureDevopsStateTracker/Services/AzureDevopsStateTrackerService.cs @@ -0,0 +1,129 @@ +using AzureDevopsStateTracker.DTOs; +using AzureDevopsStateTracker.DTOs.Create; +using AzureDevopsStateTracker.DTOs.Update; +using AzureDevopsStateTracker.Entities; +using AzureDevopsStateTracker.Extensions; +using AzureDevopsStateTracker.Interfaces; +using AzureDevopsStateTracker.Interfaces.Internals; +using System; +using System.Linq; + +namespace AzureDevopsStateTracker.Services +{ + public class AzureDevopsStateTrackerService : IAzureDevopsStateTrackerService + { + public readonly IWorkItemRepository _workItemRepository; + public readonly IWorkItemAdapter _workItemAdapter; + + public AzureDevopsStateTrackerService( + IWorkItemAdapter workItemAdapter, IWorkItemRepository workItemRepository) + { + _workItemAdapter = workItemAdapter; + _workItemRepository = workItemRepository; + } + + public void Create(CreateWorkItemDTO create) + { + var workItem = new WorkItem(create.Resource.Id); + + workItem.Update(create.Resource.Fields.Title, + create.Resource.Fields.TeamProject, + create.Resource.Fields.AreaPath, + create.Resource.Fields.IterationPath, + create.Resource.Fields.Type, + create.Resource.Fields.CreatedBy.ExtractEmail(), + create.Resource.Fields.AssignedTo.ExtractEmail(), + create.Resource.Fields.Tags, + create.Resource.Fields.Parent, + create.Resource.Fields.Effort, + create.Resource.Fields.StoryPoints, + create.Resource.Fields.OriginalEstimate, + create.Resource.Fields.Activity); + + AddWorkItemChange(workItem, create); + + _workItemRepository.Add(workItem); + _workItemRepository.SaveChanges(); + } + + public void Update(UpdatedWorkItemDTO update) + { + var workItem = _workItemRepository.GetByWorkItemId(update.Resource.WorkItemId); + if (workItem is null) + return; + + workItem.Update(update.Resource.Revision.Fields.Title, + update.Resource.Revision.Fields.TeamProject, + update.Resource.Revision.Fields.AreaPath, + update.Resource.Revision.Fields.IterationPath, + update.Resource.Revision.Fields.Type, + update.Resource.Revision.Fields.CreatedBy, + update.Resource.Revision.Fields.AssignedTo, + update.Resource.Revision.Fields.Tags, + update.Resource.Revision.Fields.Parent, + update.Resource.Revision.Fields.Effort, + update.Resource.Revision.Fields.StoryPoints, + update.Resource.Revision.Fields.OriginalEstimate, + update.Resource.Revision.Fields.Activity); + + AddWorkItemChange(workItem, update); + + _workItemRepository.Update(workItem); + _workItemRepository.SaveChanges(); + } + + public WorkItemDTO GetByWorkItemId(string workItemId) + { + var workItem = _workItemRepository.GetByWorkItemId(workItemId); + if (workItem is null) + return null; + + return _workItemAdapter.ToWorkItemDTO(workItem); + } + + #region Support Methods + public WorkItemChange ToWorkItemChange(string workItemId, string changedBy, DateTime newDate, string newState, string oldState = null, DateTime? oldDate = null) + { + return new WorkItemChange(workItemId, changedBy.ExtractEmail(), newDate, newState, oldState, oldDate); + } + + public void AddWorkItemChange(WorkItem workItem, CreateWorkItemDTO create) + { + var workItemChange = ToWorkItemChange(workItem.Id, + create.Resource.Fields.ChangedBy, + create.Resource.Fields.CreatedDate, + create.Resource.Fields.State); + + workItem.AddWorkItemChange(workItemChange); + } + + public void AddWorkItemChange(WorkItem workItem, UpdatedWorkItemDTO update) + { + var changedBy = update.Resource.Revision.Fields.ChangedBy ?? update.Resource.Fields.ChangedBy.NewValue; + var workItemChange = ToWorkItemChange(workItem.Id, + changedBy, + update.Resource.Fields.StateChangeDate.NewValue, + update.Resource.Fields.State.NewValue, + update.Resource.Fields.State.OldValue, + update.Resource.Fields.StateChangeDate.OldValue); + + workItem.AddWorkItemChange(workItemChange); + + UpdateTimeByStates(workItem); + } + + public void UpdateTimeByStates(WorkItem workItem) + { + RemoveTimeByStateFromDataBase(workItem); + + workItem.ClearTimesByState(); + workItem.AddTimesByState(workItem.CalculateTotalTimeByState()); + } + + public void RemoveTimeByStateFromDataBase(WorkItem workItem) + { + _workItemRepository.RemoveAllTimeByState(workItem.TimeByStates.ToList()); + } + #endregion + } +} \ No newline at end of file