From 5f8232809a591944285e3a75e194595b035db9f5 Mon Sep 17 00:00:00 2001 From: mahan Date: Thu, 1 Jan 2026 12:29:06 +0330 Subject: [PATCH] refactor: update project time management to use skills and improve data structure --- .../SetTimeProject/SetTimeProjectCommand.cs | 7 +- .../SetTimeProjectCommandHandler.cs | 227 +++++++++++++++--- .../SetTimeProjectCommandValidator.cs | 26 +- .../DTOs/SetTimeProjectSectionItem.cs | 5 +- .../ProjectSetTimeDetailsQuery.cs | 16 +- .../ProjectSetTimeDetailsQueryHandler.cs | 12 +- .../ProjectAgg/Entities/ProjectTask.cs | 5 + 7 files changed, 238 insertions(+), 60 deletions(-) diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommand.cs index 230754b5..b7ed7859 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommand.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommand.cs @@ -4,10 +4,15 @@ using GozareshgirProgramManager.Domain.ProjectAgg.Enums; namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimeProject; -public record SetTimeProjectCommand(List SectionItems, Guid Id, ProjectHierarchyLevel Level):IBaseCommand; +public record SetTimeProjectCommand( + List SkillItems, + Guid Id, + ProjectHierarchyLevel Level, + bool CascadeToChildren) : IBaseCommand; public class SetTimeSectionTime { public string Description { get; set; } public int Hours { get; set; } + public int Minutes { get; set; } } \ No newline at end of file diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommandHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommandHandler.cs index f6e9bafa..e88116b6 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommandHandler.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommandHandler.cs @@ -6,6 +6,8 @@ using GozareshgirProgramManager.Domain._Common.Exceptions; using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Repositories; +using GozareshgirProgramManager.Domain.SkillAgg.Repositories; +using GozareshgirProgramManager.Domain.UserAgg.Repositories; namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimeProject; @@ -15,21 +17,33 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler SetTimeForProject(SetTimeProjectCommand request, - CancellationToken cancellationToken) + private async Task AssignProject(SetTimeProjectCommand request) { var project = await _projectRepository.GetWithFullHierarchyAsync(request.Id); - if (project == null) + if (project is null) { return OperationResult.NotFound("پروژه یافت نشد"); - return OperationResult.NotFound("���� ���� ���"); } - long? addedByUserId = _userId; + var skillItems = request.SkillItems.Where(x=>x.UserId is > 0).ToList(); + + // تخصیص در سطح پروژه + foreach (var item in skillItems) + { + var skill = await _skillRepository.GetByIdAsync(item.SkillId); + if (skill is null) + { + return OperationResult.NotFound($"مهارت با شناسه {item.SkillId} یافت نشد"); + } - // تنظیم زمان برای تمام sections در تمام فازها و تسک‌های پروژه + // بررسی و به‌روزرسانی یا اضافه کردن ProjectSection + var existingSection = project.ProjectSections.FirstOrDefault(s => s.SkillId == item.SkillId); + if (existingSection != null) + { + // اگر وجود داشت، فقط userId را به‌روزرسانی کن + existingSection.UpdateUser(item.UserId.Value); + } + else + { + // اگر وجود نداشت، اضافه کن + var newSection = new ProjectSection(project.Id, item.UserId.Value, item.SkillId); + await _projectSectionRepository.CreateAsync(newSection); + } + } + + // حالا برای تمام فازها و تسک‌ها cascade کن foreach (var phase in project.Phases) { - foreach (var task in phase.Tasks) + // اگر CascadeToChildren true است یا فاز override ندارد + if (request.CascadeToChildren || !phase.HasAssignmentOverride) { - foreach (var section in task.Sections) + // برای phase هم باید section‌ها را به‌روزرسانی کنیم + foreach (var item in skillItems ) { - var sectionItem = request.SectionItems.FirstOrDefault(si => si.SectionId == section.Id); - if (sectionItem != null) + var existingSection = phase.PhaseSections.FirstOrDefault(s => s.SkillId == item.SkillId); + if (existingSection != null) { - SetSectionTime(section, sectionItem, addedByUserId); + existingSection.Update(item.UserId.Value, item.SkillId); + } + else + { + var newPhaseSection = new PhaseSection(phase.Id, item.UserId.Value, item.SkillId); + await _phaseSectionRepository.CreateAsync(newPhaseSection); + } + } + + foreach (var task in phase.Tasks) + { + // اگر CascadeToChildren true است یا تسک override ندارد + if (request.CascadeToChildren || !task.HasAssignmentOverride) + { + foreach (var item in skillItems) + { + var section = task.Sections.FirstOrDefault(s => s.SkillId == item.SkillId); + if (section != null) + { + // استفاده از TransferToUser + if (section.CurrentAssignedUserId != item.UserId) + { + if (section.CurrentAssignedUserId > 0) + { + section.TransferToUser(section.CurrentAssignedUserId, item.UserId.Value); + } + else + { + section.AssignToUser(item.UserId.Value); + } + } + } + else + { + var newTaskSection = new TaskSection(task.Id, item.SkillId, item.UserId.Value); + await _taskSectionRepository.CreateAsync(newTaskSection); + } + } } } } } - await _unitOfWork.SaveChangesAsync(cancellationToken); + await _unitOfWork.SaveChangesAsync(); return OperationResult.Success(); } - private async Task SetTimeForProjectPhase(SetTimeProjectCommand request, - CancellationToken cancellationToken) + private async Task AssignProjectPhase(SetTimeProjectCommand request) { var phase = await _projectPhaseRepository.GetWithTasksAsync(request.Id); - if (phase == null) + if (phase is null) { return OperationResult.NotFound("فاز پروژه یافت نشد"); - return OperationResult.NotFound("��� ���� ���� ���"); } - long? addedByUserId = _userId; + // تخصیص در سطح فاز + foreach (var item in request.SkillItems) + { + var skill = await _skillRepository.GetByIdAsync(item.SkillId); + if (skill is null) + { + return OperationResult.NotFound($"مهارت با شناسه {item.SkillId} یافت نشد"); + } + } - // تنظیم زمان برای تمام sections در تمام تسک‌های این فاز + // علامت‌گذاری که این فاز نسبت به parent متمایز است + phase.MarkAsOverridden(); + + var skillItems = request.SkillItems.Where(x=>x.UserId is > 0).ToList(); + // به‌روزرسانی یا اضافه کردن PhaseSection + foreach (var item in skillItems) + { + var existingSection = phase.PhaseSections.FirstOrDefault(s => s.SkillId == item.SkillId); + if (existingSection != null) + { + // اگر وجود داشت، فقط userId را به‌روزرسانی کن + existingSection.Update(item.UserId!.Value, item.SkillId); + } + else + { + // اگر وجود نداشت، اضافه کن + var newPhaseSection = new PhaseSection(phase.Id, item.UserId!.Value, item.SkillId); + await _phaseSectionRepository.CreateAsync(newPhaseSection); + } + } + + // cascade به تمام تسک‌ها foreach (var task in phase.Tasks) { - foreach (var section in task.Sections) + // اگر CascadeToChildren true است یا تسک override ندارد + if (request.CascadeToChildren || !task.HasAssignmentOverride) { - var sectionItem = request.SectionItems.FirstOrDefault(si => si.SectionId == section.Id); - if (sectionItem != null) + foreach (var item in skillItems) { - SetSectionTime(section, sectionItem, addedByUserId); + var section = task.Sections.FirstOrDefault(s => s.SkillId == item.SkillId); + if (section != null) + { + // استفاده از TransferToUser + if (section.CurrentAssignedUserId != item.UserId) + { + if (section.CurrentAssignedUserId > 0) + { + section.TransferToUser(section.CurrentAssignedUserId, item.UserId!.Value); + } + else + { + section.AssignToUser(item.UserId!.Value); + } + } + } + else + { + var newTaskSection = new TaskSection(task.Id, item.SkillId, item.UserId!.Value); + await _taskSectionRepository.CreateAsync(newTaskSection); + } } } } - await _unitOfWork.SaveChangesAsync(cancellationToken); + await _unitOfWork.SaveChangesAsync(); return OperationResult.Success(); } + private async Task SetTimeForProjectTask(SetTimeProjectCommand request, CancellationToken cancellationToken) { @@ -116,21 +243,51 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandlerx.UserId is > 0).ToList(); + + task.ClearTaskSections(); + + foreach (var skillItem in validSkills) { - var sectionItem = request.SectionItems.FirstOrDefault(si => si.SectionId == section.Id); - if (sectionItem != null) - { - SetSectionTime(section, sectionItem, addedByUserId); - } - } + var section = task.Sections.FirstOrDefault(s => s.SkillId == skillItem.SkillId); + if (!_userRepository.Exists(x=>x.Id == skillItem.UserId!.Value)) + { + throw new BadRequestException("کاربر با شناسه یافت نشد."); + } + + if (section == null) + { + var taskSection = new TaskSection(task.Id, + skillItem.SkillId, skillItem.UserId!.Value); + + task.AddSection(taskSection); + section = taskSection; + } + else + { + if (section.CurrentAssignedUserId != skillItem.UserId) + { + if (section.CurrentAssignedUserId > 0) + { + section.TransferToUser(section.CurrentAssignedUserId, skillItem.UserId!.Value); + } + else + { + section.AssignToUser(skillItem.UserId!.Value); + } + } + } + + SetSectionTime(section, skillItem, addedByUserId); + + } await _unitOfWork.SaveChangesAsync(cancellationToken); return OperationResult.Success(); } - private void SetSectionTime(TaskSection section, SetTimeProjectSectionItem sectionItem, long? addedByUserId) + private void SetSectionTime(TaskSection section, SetTimeProjectSkillItem sectionItem, long? addedByUserId) { var initData = sectionItem.InitData; var initialTime = TimeSpan.FromHours(initData.Hours); diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommandValidator.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommandValidator.cs index c47ef658..89f1d33e 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommandValidator.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommandValidator.cs @@ -13,19 +13,15 @@ public class SetTimeProjectCommandValidator:AbstractValidator x.SectionItems) - .SetValidator(command => new SetTimeProjectSectionItemValidator()); - - RuleFor(x => x.SectionItems) - .Must(sectionItems => sectionItems.Any(si => si.InitData?.Hours > 0)) - .WithMessage("حداقل یکی از بخش‌ها باید مقدار ساعت معتبری داشته باشد."); + RuleForEach(x => x.SkillItems) + .SetValidator(command => new SetTimeProjectSkillItemValidator()); } } -public class SetTimeProjectSectionItemValidator:AbstractValidator +public class SetTimeProjectSkillItemValidator:AbstractValidator { - public SetTimeProjectSectionItemValidator() + public SetTimeProjectSkillItemValidator() { - RuleFor(x=>x.SectionId) + RuleFor(x=>x.SkillId) .NotEmpty() .NotNull() .WithMessage("شناسه بخش نمی‌تواند خالی باشد."); @@ -47,6 +43,18 @@ public class AdditionalTimeDataValidator: AbstractValidator .GreaterThanOrEqualTo(0) .WithMessage("ساعت نمی‌تواند منفی باشد."); + RuleFor(x => x.Hours) + .LessThan(1_000) + .WithMessage("ساعت باید کمتر از 1000 باشد."); + + RuleFor(x => x.Minutes) + .GreaterThanOrEqualTo(0) + .WithMessage("دقیقه نمی‌تواند منفی باشد."); + + RuleFor(x => x.Minutes) + .LessThan(60) + .WithMessage("دقیقه باید بین 0 تا 59 باشد."); + RuleFor(x=>x.Description) .MaximumLength(500) .WithMessage("توضیحات نمی‌تواند بیشتر از 500 کاراکتر باشد."); diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/DTOs/SetTimeProjectSectionItem.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/DTOs/SetTimeProjectSectionItem.cs index 04566e4e..75fdaaf8 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/DTOs/SetTimeProjectSectionItem.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/DTOs/SetTimeProjectSectionItem.cs @@ -2,9 +2,10 @@ using GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimePro namespace GozareshgirProgramManager.Application.Modules.Projects.DTOs; -public class SetTimeProjectSectionItem +public class SetTimeProjectSkillItem { - public Guid SectionId { get; set; } + public Guid SkillId { get; set; } + public long? UserId { get; set; } public SetTimeSectionTime InitData { get; set; } public List AdditionalTime { get; set; } = []; } \ No newline at end of file diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQuery.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQuery.cs index 4b2c08b9..764d5127 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQuery.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQuery.cs @@ -6,18 +6,19 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.Project public record ProjectSetTimeDetailsQuery(Guid TaskId) : IBaseQuery; public record ProjectSetTimeResponse( - List SectionItems, + List SectionItems, Guid Id, ProjectHierarchyLevel Level); -public record ProjectSetTimeResponseSections +public record ProjectSetTimeResponseSkill { + public Guid SkillId { get; init; } public string SkillName { get; init; } - public string UserName { get; init; } - public int InitialTime { get; set; } + public long UserId { get; set; } + public string UserFullName { get; init; } + public int InitialHours { get; set; } + public int InitialMinutes { get; set; } public string InitialDescription { get; set; } - public int TotalEstimateTime { get; init; } - public int TotalAdditionalTime { get; init; } public string InitCreationTime { get; init; } public List AdditionalTimes { get; init; } public Guid SectionId { get; set; } @@ -25,6 +26,7 @@ public record ProjectSetTimeResponseSections public class ProjectSetTimeResponseSectionAdditionalTime { - public int Time { get; init; } + public int Hours { get; init; } + public int Minutes { get; init; } public string Description { get; init; } } diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryHandler.cs index f99f3370..38e3d8e9 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryHandler.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryHandler.cs @@ -53,22 +53,22 @@ public class ProjectSetTimeDetailsQueryHandler { var user = users.FirstOrDefault(x => x.Id == ts.OriginalAssignedUserId); var skill = skills.FirstOrDefault(x => x.Id == ts.SkillId); - return new ProjectSetTimeResponseSections + return new ProjectSetTimeResponseSkill { AdditionalTimes = ts.AdditionalTimes .Select(x => new ProjectSetTimeResponseSectionAdditionalTime { Description = x.Reason ?? "", - Time = (int)x.Hours.TotalHours + Hours = (int)x.Hours.TotalHours, + Minutes = x.Hours.Minutes }).ToList(), InitCreationTime = ts.CreationDate.ToFarsi(), SkillName = skill?.Name ?? "", - TotalAdditionalTime = (int)ts.GetTotalAdditionalTime().TotalHours, - TotalEstimateTime = (int)ts.FinalEstimatedHours.TotalHours, - UserName = user?.UserName ?? "", + UserFullName = user?.FullName ?? "", SectionId = ts.Id, InitialDescription = ts.InitialDescription ?? "", - InitialTime = (int)ts.InitialEstimatedHours.TotalHours + InitialHours = (int)ts.InitialEstimatedHours.TotalHours, + InitialMinutes = ts.InitialEstimatedHours.Minutes, }; }).ToList(), task.Id, diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectTask.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectTask.cs index 863e332f..71de7615 100644 --- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectTask.cs +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectTask.cs @@ -246,4 +246,9 @@ public class ProjectTask : ProjectHierarchyNode } #endregion + + public void ClearTaskSections() + { + _sections.Clear(); + } }