From 5f8232809a591944285e3a75e194595b035db9f5 Mon Sep 17 00:00:00 2001 From: mahan Date: Thu, 1 Jan 2026 12:29:06 +0330 Subject: [PATCH 01/12] 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(); + } } From f99f199a7755c964ea1105085e36f09d0acf9551 Mon Sep 17 00:00:00 2001 From: mahan Date: Thu, 1 Jan 2026 13:16:52 +0330 Subject: [PATCH 02/12] refactor: rename SectionItems to SkillItems and add CreationDate to project time details --- .../ProjectSetTimeDetails/ProjectSetTimeDetailsQuery.cs | 3 ++- .../ProjectSetTimeDetailsQueryHandler.cs | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) 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 764d5127..d0bfd45f 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,7 +6,7 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.Project public record ProjectSetTimeDetailsQuery(Guid TaskId) : IBaseQuery; public record ProjectSetTimeResponse( - List SectionItems, + List SkillItems, Guid Id, ProjectHierarchyLevel Level); @@ -29,4 +29,5 @@ public class ProjectSetTimeResponseSectionAdditionalTime public int Hours { get; init; } public int Minutes { get; init; } public string Description { get; init; } + public string CreationDate { get; set; } } 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 38e3d8e9..f57dd1c3 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 @@ -60,7 +60,9 @@ public class ProjectSetTimeDetailsQueryHandler { Description = x.Reason ?? "", Hours = (int)x.Hours.TotalHours, - Minutes = x.Hours.Minutes + Minutes = x.Hours.Minutes, + CreationDate = x.CreationDate.ToFarsi() + }).ToList(), InitCreationTime = ts.CreationDate.ToFarsi(), SkillName = skill?.Name ?? "", @@ -70,7 +72,7 @@ public class ProjectSetTimeDetailsQueryHandler InitialHours = (int)ts.InitialEstimatedHours.TotalHours, InitialMinutes = ts.InitialEstimatedHours.Minutes, }; - }).ToList(), + }).OrderBy(x=>x.SkillId).ToList(), task.Id, ProjectHierarchyLevel.Task); From 385a885c93f3cff0f1d654f8bdc2cbb51bbc4475 Mon Sep 17 00:00:00 2001 From: mahan Date: Thu, 1 Jan 2026 14:13:27 +0330 Subject: [PATCH 03/12] feat: add TotalHours and Minutes to project and task details for improved time tracking --- .../GetProjectsList/GetProjectListDto.cs | 2 + .../GetProjectsListQueryHandler.cs | 70 ++++++++++++------- .../ProjectSetTimeDetailsQueryHandler.cs | 2 + 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectListDto.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectListDto.cs index 74a58946..7066148e 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectListDto.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectListDto.cs @@ -11,6 +11,8 @@ public record GetProjectListDto public bool HasFront { get; set; } public bool HasBackend { get; set; } public bool HasDesign { get; set; } + public int TotalHours { get; set; } + public int Minutes { get; set; } } diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListQueryHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListQueryHandler.cs index 1815c2bc..e79b7fe4 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListQueryHandler.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListQueryHandler.cs @@ -53,17 +53,19 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler p.CreationDate) .ToListAsync(cancellationToken); var result = new List(); - + foreach (var project in projects) { - var percentage = await CalculateProjectPercentage(project, cancellationToken); + var (percentage, totalTime) = await CalculateProjectPercentage(project, cancellationToken); result.Add(new GetProjectListDto { Id = project.Id, Name = project.Name, Level = ProjectHierarchyLevel.Project, ParentId = null, - Percentage = percentage + Percentage = percentage, + TotalHours = (int)totalTime.TotalHours, + Minutes = totalTime.Minutes, }); } @@ -86,14 +88,16 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler CalculateProjectPercentage(Project project, CancellationToken cancellationToken) + private async Task<(int Percentage, TimeSpan TotalTime)> CalculateProjectPercentage(Project project, CancellationToken cancellationToken) { // گرفتن تمام فازهای پروژه var phases = await _context.ProjectPhases @@ -219,20 +225,24 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler(); + var totalTime = TimeSpan.Zero; + foreach (var phase in phases) { - var phasePercentage = await CalculatePhasePercentage(phase, cancellationToken); + var (phasePercentage, phaseTime) = await CalculatePhasePercentage(phase, cancellationToken); phasePercentages.Add(phasePercentage); + totalTime += phaseTime; } - return phasePercentages.Any() ? (int)phasePercentages.Average() : 0; + var averagePercentage = phasePercentages.Any() ? (int)phasePercentages.Average() : 0; + return (averagePercentage, totalTime); } - private async Task CalculatePhasePercentage(ProjectPhase phase, CancellationToken cancellationToken) + private async Task<(int Percentage, TimeSpan TotalTime)> CalculatePhasePercentage(ProjectPhase phase, CancellationToken cancellationToken) { // گرفتن تمام تسک‌های فاز var tasks = await _context.ProjectTasks @@ -240,20 +250,24 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler(); + var totalTime = TimeSpan.Zero; + foreach (var task in tasks) { - var taskPercentage = await CalculateTaskPercentage(task, cancellationToken); + var (taskPercentage, taskTime) = await CalculateTaskPercentage(task, cancellationToken); taskPercentages.Add(taskPercentage); + totalTime += taskTime; } - return taskPercentages.Any() ? (int)taskPercentages.Average() : 0; + var averagePercentage = taskPercentages.Any() ? (int)taskPercentages.Average() : 0; + return (averagePercentage, totalTime); } - private async Task CalculateTaskPercentage(ProjectTask task, CancellationToken cancellationToken) + private async Task<(int Percentage, TimeSpan TotalTime)> CalculateTaskPercentage(ProjectTask task, CancellationToken cancellationToken) { // گرفتن تمام سکشن‌های تسک با activities var sections = await _context.TaskSections @@ -262,33 +276,39 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler(); + var totalTime = TimeSpan.Zero; + foreach (var section in sections) { - var sectionPercentage = CalculateSectionPercentage(section); + var (sectionPercentage, sectionTime) = CalculateSectionPercentage(section); sectionPercentages.Add(sectionPercentage); + totalTime += sectionTime; } - return sectionPercentages.Any() ? (int)sectionPercentages.Average() : 0; + var averagePercentage = sectionPercentages.Any() ? (int)sectionPercentages.Average() : 0; + return (averagePercentage, totalTime); } - private static int CalculateSectionPercentage(TaskSection section) + private static (int Percentage, TimeSpan TotalTime) CalculateSectionPercentage(TaskSection section) { // محاسبه کل زمان تخمین زده شده (اولیه + اضافی) var totalEstimatedHours = section.FinalEstimatedHours.TotalHours; - if (totalEstimatedHours <= 0) - return 0; - // محاسبه کل زمان صرف شده از activities - var totalSpentHours = section.Activities.Sum(a => a.GetTimeSpent().TotalHours); + var totalSpentTime = TimeSpan.FromHours(section.Activities.Sum(a => a.GetTimeSpent().TotalHours)); + + if (totalEstimatedHours <= 0) + return (0, section.FinalEstimatedHours); + + var totalSpentHours = totalSpentTime.TotalHours; // محاسبه درصد (حداکثر 100%) var percentage = (totalSpentHours / totalEstimatedHours) * 100; - return Math.Min((int)Math.Round(percentage), 100); + return (Math.Min((int)Math.Round(percentage), 100), section.FinalEstimatedHours); } } 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 f57dd1c3..79271ee4 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 @@ -71,6 +71,8 @@ public class ProjectSetTimeDetailsQueryHandler InitialDescription = ts.InitialDescription ?? "", InitialHours = (int)ts.InitialEstimatedHours.TotalHours, InitialMinutes = ts.InitialEstimatedHours.Minutes, + UserId = ts.OriginalAssignedUserId, + SkillId = ts.SkillId, }; }).OrderBy(x=>x.SkillId).ToList(), task.Id, From 6f64ee1ce48f75655cd3f2a024a50eb3f9ec0d3f Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 4 Jan 2026 13:01:43 +0330 Subject: [PATCH 04/12] refactor: streamline ProjectSetTimeDetailsQueryHandler to enhance skill and user data retrieval --- .../ProjectSetTimeDetailsQueryHandler.cs | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) 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 79271ee4..a41c4168 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 @@ -40,22 +40,20 @@ public class ProjectSetTimeDetailsQueryHandler .Where(x => userIds.Contains(x.Id)) .AsNoTracking() .ToListAsync(cancellationToken); - var skillIds = task.Sections.Select(x => x.SkillId) - .Distinct().ToList(); var skills = await _context.Skills - .Where(x => skillIds.Contains(x.Id)) .AsNoTracking() .ToListAsync(cancellationToken); var res = new ProjectSetTimeResponse( - task.Sections.Select(ts => + skills.Select(skill => { - var user = users.FirstOrDefault(x => x.Id == ts.OriginalAssignedUserId); - var skill = skills.FirstOrDefault(x => x.Id == ts.SkillId); + var section = task.Sections + .FirstOrDefault(x => x.SkillId == skill.Id); + var user = users.FirstOrDefault(x => x.Id == section?.OriginalAssignedUserId); return new ProjectSetTimeResponseSkill { - AdditionalTimes = ts.AdditionalTimes + AdditionalTimes = section?.AdditionalTimes .Select(x => new ProjectSetTimeResponseSectionAdditionalTime { Description = x.Reason ?? "", @@ -63,16 +61,16 @@ public class ProjectSetTimeDetailsQueryHandler Minutes = x.Hours.Minutes, CreationDate = x.CreationDate.ToFarsi() - }).ToList(), - InitCreationTime = ts.CreationDate.ToFarsi(), + }).OrderBy(x=>x.CreationDate).ToList()??[], + InitCreationTime = section?.CreationDate.ToFarsi()??"", SkillName = skill?.Name ?? "", UserFullName = user?.FullName ?? "", - SectionId = ts.Id, - InitialDescription = ts.InitialDescription ?? "", - InitialHours = (int)ts.InitialEstimatedHours.TotalHours, - InitialMinutes = ts.InitialEstimatedHours.Minutes, - UserId = ts.OriginalAssignedUserId, - SkillId = ts.SkillId, + SectionId = section?.Id??Guid.Empty, + InitialDescription = section?.InitialDescription ?? "", + InitialHours = (int)(section?.InitialEstimatedHours.TotalHours ?? 0), + InitialMinutes = section?.InitialEstimatedHours.Minutes??0, + UserId = section?.OriginalAssignedUserId??0, + SkillId = task.Id, }; }).OrderBy(x=>x.SkillId).ToList(), task.Id, From 4ada29a98a440239670905b436a09fd570969505 Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 4 Jan 2026 13:51:36 +0330 Subject: [PATCH 05/12] feat: enhance ProjectSetTimeDetailsQuery to support multiple hierarchy levels and improve data retrieval --- .../ProjectSetTimeDetailsQuery.cs | 2 +- .../ProjectSetTimeDetailsQueryHandler.cs | 175 ++++++++++++++---- .../ProjectSetTimeDetailsQueryValidator.cs | 13 +- 3 files changed, 154 insertions(+), 36 deletions(-) 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 d0bfd45f..a3ac883d 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 @@ -3,7 +3,7 @@ using GozareshgirProgramManager.Domain.ProjectAgg.Enums; namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails; -public record ProjectSetTimeDetailsQuery(Guid TaskId) +public record ProjectSetTimeDetailsQuery(Guid Id, ProjectHierarchyLevel Level) : IBaseQuery; public record ProjectSetTimeResponse( List SkillItems, 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 a41c4168..c3f48bfa 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 @@ -22,17 +22,30 @@ public class ProjectSetTimeDetailsQueryHandler public async Task> Handle(ProjectSetTimeDetailsQuery request, CancellationToken cancellationToken) + { + return request.Level switch + { + ProjectHierarchyLevel.Task => await GetTaskSetTimeDetails(request.Id, request.Level, cancellationToken), + ProjectHierarchyLevel.Phase => await GetPhaseSetTimeDetails(request.Id, request.Level, cancellationToken), + ProjectHierarchyLevel.Project => await GetProjectSetTimeDetails(request.Id, request.Level, cancellationToken), + _ => OperationResult.Failure("سطح معادل نامعتبر است") + }; + } + + private async Task> GetTaskSetTimeDetails(Guid id, ProjectHierarchyLevel level, + CancellationToken cancellationToken) { var task = await _context.ProjectTasks - .Where(p => p.Id == request.TaskId) + .Where(p => p.Id == id) .Include(x => x.Sections) .ThenInclude(x => x.AdditionalTimes).AsNoTracking() .FirstOrDefaultAsync(cancellationToken); if (task == null) { - return OperationResult.NotFound("Project not found"); + return OperationResult.NotFound("تسک یافت نشد"); } + var userIds = task.Sections.Select(x => x.OriginalAssignedUserId) .Distinct().ToList(); @@ -41,41 +54,141 @@ public class ProjectSetTimeDetailsQueryHandler .AsNoTracking() .ToListAsync(cancellationToken); - var skills = await _context.Skills + var skills = await _context.Skills .AsNoTracking() .ToListAsync(cancellationToken); var res = new ProjectSetTimeResponse( - skills.Select(skill => + skills.Select(skill => + { + var section = task.Sections + .FirstOrDefault(x => x.SkillId == skill.Id); + var user = users.FirstOrDefault(x => x.Id == section?.OriginalAssignedUserId); + return new ProjectSetTimeResponseSkill { - var section = task.Sections - .FirstOrDefault(x => x.SkillId == skill.Id); - var user = users.FirstOrDefault(x => x.Id == section?.OriginalAssignedUserId); - return new ProjectSetTimeResponseSkill - { - AdditionalTimes = section?.AdditionalTimes - .Select(x => new ProjectSetTimeResponseSectionAdditionalTime - { - Description = x.Reason ?? "", - Hours = (int)x.Hours.TotalHours, - Minutes = x.Hours.Minutes, - CreationDate = x.CreationDate.ToFarsi() - - }).OrderBy(x=>x.CreationDate).ToList()??[], - InitCreationTime = section?.CreationDate.ToFarsi()??"", - SkillName = skill?.Name ?? "", - UserFullName = user?.FullName ?? "", - SectionId = section?.Id??Guid.Empty, - InitialDescription = section?.InitialDescription ?? "", - InitialHours = (int)(section?.InitialEstimatedHours.TotalHours ?? 0), - InitialMinutes = section?.InitialEstimatedHours.Minutes??0, - UserId = section?.OriginalAssignedUserId??0, - SkillId = task.Id, - }; - }).OrderBy(x=>x.SkillId).ToList(), - task.Id, - ProjectHierarchyLevel.Task); + AdditionalTimes = section?.AdditionalTimes + .Select(x => new ProjectSetTimeResponseSectionAdditionalTime + { + Description = x.Reason ?? "", + Hours = (int)x.Hours.TotalHours, + Minutes = x.Hours.Minutes, + CreationDate = x.CreationDate.ToFarsi() + }).OrderBy(x => x.CreationDate).ToList() ?? [], + InitCreationTime = section?.CreationDate.ToFarsi() ?? "", + SkillName = skill?.Name ?? "", + UserFullName = user?.FullName ?? "", + SectionId = section?.Id ?? Guid.Empty, + InitialDescription = section?.InitialDescription ?? "", + InitialHours = (int)(section?.InitialEstimatedHours.TotalHours ?? 0), + InitialMinutes = section?.InitialEstimatedHours.Minutes ?? 0, + UserId = section?.OriginalAssignedUserId ?? 0, + SkillId = task.Id, + }; + }).OrderBy(x => x.SkillId).ToList(), + task.Id, + level); + return OperationResult.Success(res); + } + + private async Task> GetPhaseSetTimeDetails(Guid id, ProjectHierarchyLevel level, + CancellationToken cancellationToken) + { + var phase = await _context.ProjectPhases + .Where(p => p.Id == id) + .Include(x => x.PhaseSections).AsNoTracking() + .FirstOrDefaultAsync(cancellationToken); + + if (phase == null) + { + return OperationResult.NotFound("فاز یافت نشد"); + } + + var userIds = phase.PhaseSections.Select(x => x.UserId) + .Distinct().ToList(); + + var users = await _context.Users + .Where(x => userIds.Contains(x.Id)) + .AsNoTracking() + .ToListAsync(cancellationToken); + + var skills = await _context.Skills + .AsNoTracking() + .ToListAsync(cancellationToken); + + var res = new ProjectSetTimeResponse( + skills.Select(skill => + { + var section = phase.PhaseSections + .FirstOrDefault(x => x.SkillId == skill.Id); + var user = users.FirstOrDefault(x => x.Id == section?.UserId); + return new ProjectSetTimeResponseSkill + { + AdditionalTimes = [], + InitCreationTime = "", + SkillName = skill.Name ?? "", + UserFullName = user?.FullName ?? "", + SectionId = section?.Id ?? Guid.Empty, + InitialDescription = "", + InitialHours = 0, + InitialMinutes = 0, + UserId = section?.UserId ?? 0, + SkillId = skill.Id, + }; + }).OrderBy(x => x.SkillId).ToList(), + phase.Id, + level); + + return OperationResult.Success(res); + } + + private async Task> GetProjectSetTimeDetails(Guid id, ProjectHierarchyLevel level, + CancellationToken cancellationToken) + { + var project = await _context.Projects + .Where(p => p.Id == id) + .Include(x => x.ProjectSections).AsNoTracking() + .FirstOrDefaultAsync(cancellationToken); + + if (project == null) + { + return OperationResult.NotFound("پروژه یافت نشد"); + } + + var userIds = project.ProjectSections.Select(x => x.UserId) + .Distinct().ToList(); + + var users = await _context.Users + .Where(x => userIds.Contains(x.Id)) + .AsNoTracking() + .ToListAsync(cancellationToken); + + var skills = await _context.Skills + .AsNoTracking() + .ToListAsync(cancellationToken); + + var res = new ProjectSetTimeResponse( + skills.Select(skill => + { + var section = project.ProjectSections + .FirstOrDefault(x => x.SkillId == skill.Id); + var user = users.FirstOrDefault(x => x.Id == section?.UserId); + return new ProjectSetTimeResponseSkill + { + AdditionalTimes = [], + InitCreationTime = "", + SkillName = skill.Name ?? "", + UserFullName = user?.FullName ?? "", + SectionId = section?.Id ?? Guid.Empty, + InitialDescription = "", + InitialHours = 0, + InitialMinutes = 0, + UserId = section?.UserId ?? 0, + SkillId = skill.Id, + }; + }).OrderBy(x => x.SkillId).ToList(), + project.Id, + level); return OperationResult.Success(res); } diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryValidator.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryValidator.cs index 13952f46..a91c0756 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryValidator.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryValidator.cs @@ -1,13 +1,18 @@ -using FluentValidation; +using FluentValidation; +using GozareshgirProgramManager.Domain.ProjectAgg.Enums; namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails; -public class ProjectSetTimeDetailsQueryValidator:AbstractValidator +public class ProjectSetTimeDetailsQueryValidator : AbstractValidator { public ProjectSetTimeDetailsQueryValidator() { - RuleFor(x => x.TaskId) + RuleFor(x => x.Id) .NotEmpty() - .WithMessage("شناسه پروژه نمی‌تواند خالی باشد."); + .WithMessage("شناسه نمی‌تواند خالی باشد."); + + RuleFor(x => x.Level) + .IsInEnum() + .WithMessage("سطح معادل نامعتبر است."); } } \ No newline at end of file From 0bfcde6a3f8b87ebad71d4d47edd836158578e78 Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 4 Jan 2026 14:13:29 +0330 Subject: [PATCH 06/12] feat: add validation to prevent users from starting multiple sections in progress --- .../ChangeStatusSectionCommandHandler.cs | 5 ++++- .../ProjectSetTimeDetailsQueryValidator.cs | 2 +- .../ProjectAgg/Repositories/ITaskSectionRepository.cs | 1 + .../Persistence/Repositories/TaskSectionRepository.cs | 7 +++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs index 9e6803f5..d5d0cd62 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs @@ -52,7 +52,10 @@ public class ChangeStatusSectionCommandHandler : IBaseCommandHandler Task> GetAssignedToUserAsync(long userId); Task> GetActiveSectionsIncludeAllAsync(CancellationToken cancellationToken); + Task HasUserAnyInProgressSectionAsync(long userId, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs index 89fb05fd..1b29e154 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs @@ -45,4 +45,11 @@ public class TaskSectionRepository:RepositoryBase,ITaskSection .Include(x => x.AdditionalTimes) .ToListAsync(cancellationToken); } + + public async Task HasUserAnyInProgressSectionAsync(long userId, CancellationToken cancellationToken = default) + { + return await _context.TaskSections + .AnyAsync(x => x.CurrentAssignedUserId == userId && x.Status == TaskSectionStatus.InProgress, + cancellationToken); + } } \ No newline at end of file From 1f365f3642327e9d7d3c661906705cbe5c149279 Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 4 Jan 2026 15:14:28 +0330 Subject: [PATCH 07/12] feat: implement project hierarchy search functionality with validation --- .../GetProjectHierarchySearchQuery.cs | 11 ++ .../GetProjectHierarchySearchQueryHandler.cs | 145 ++++++++++++++++++ ...GetProjectHierarchySearchQueryValidator.cs | 18 +++ .../GetProjectHierarchySearchResponse.cs | 8 + .../ProjectHierarchySearchResultDto.cs | 36 +++++ .../ProgramManager/ProjectController.cs | 12 +- 6 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQuery.cs create mode 100644 ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryHandler.cs create mode 100644 ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryValidator.cs create mode 100644 ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchResponse.cs create mode 100644 ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/ProjectHierarchySearchResultDto.cs diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQuery.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQuery.cs new file mode 100644 index 00000000..22f6c800 --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQuery.cs @@ -0,0 +1,11 @@ +using GozareshgirProgramManager.Application._Common.Interfaces; + +namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch; + +/// +/// درخواست جستجو در سراسر سلسله‌مراتب پروژه (پروژه، فاز، تسک). +/// نتایج با اطلاعات مسیر سلسله‌مراتب برای پشتیبانی از ناوبری درخت در رابط کاربری بازگردانده می‌شود. +/// +public record GetProjectHierarchySearchQuery( + string SearchQuery) : IBaseQuery; + \ No newline at end of file diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryHandler.cs new file mode 100644 index 00000000..2e20e5e6 --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryHandler.cs @@ -0,0 +1,145 @@ +using GozareshgirProgramManager.Application._Common.Interfaces; +using GozareshgirProgramManager.Application._Common.Models; +using GozareshgirProgramManager.Domain.ProjectAgg.Enums; +using Microsoft.EntityFrameworkCore; + +namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch; + +/// +/// Handler برای درخواست جستجوی سراسری در سلسله‌مراتب پروژه. +/// این handler در تمام سطح‌های پروژه، فاز و تسک جستجو می‌کند و از تمام فیلدهای متنی (نام، توضیحات) استفاده می‌کند. +/// همچنین در زیرمجموعه‌های هر سطح (ProjectSections، PhaseSections، TaskSections) جستجو می‌کند. +/// +public class GetProjectHierarchySearchQueryHandler : IBaseQueryHandler +{ + private readonly IProgramManagerDbContext _context; + private const int MaxResults = 50; + + public GetProjectHierarchySearchQueryHandler(IProgramManagerDbContext context) + { + _context = context; + } + + public async Task> Handle( + GetProjectHierarchySearchQuery request, + CancellationToken cancellationToken) + { + var searchQuery = request.SearchQuery.ToLower(); + var results = new List(); + + // جستجو در پروژه‌ها و ProjectSections + var projects = await SearchProjects(searchQuery, cancellationToken); + results.AddRange(projects); + + // جستجو در فازها و PhaseSections + var phases = await SearchPhases(searchQuery, cancellationToken); + results.AddRange(phases); + + // جستجو در تسک‌ها و TaskSections + var tasks = await SearchTasks(searchQuery, cancellationToken); + results.AddRange(tasks); + + // مرتب‌سازی نتایج: ابتدا بر اساس سطح سلسله‌مراتب (پروژه → فاز → تسک)، سپس بر اساس نام + var sortedResults = results + .OrderBy(r => GetLevelOrder(r.Level)) + .ThenBy(r => r.Title) + .Take(MaxResults) + .ToList(); + + var response = new GetProjectHierarchySearchResponse(sortedResults); + return OperationResult.Success(response); + } + + /// + /// جستجو در جدول پروژه‌ها (نام، توضیحات) و ProjectSections (نام مهارت، توضیحات اولیه) + /// + private async Task> SearchProjects( + string searchQuery, + CancellationToken cancellationToken) + { + var projects = await _context.Projects + .Where(p => + p.Name.ToLower().Contains(searchQuery) || + (p.Description != null && p.Description.ToLower().Contains(searchQuery))) + .Select(p => new ProjectHierarchySearchResultDto + { + Id = p.Id, + Title = p.Name, + Level = ProjectHierarchyLevel.Project, + ProjectId = p.Id, + PhaseId = null + }) + .ToListAsync(cancellationToken); + + return projects; + } + + /// + /// جستجو در جدول فازهای پروژه (نام، توضیحات) و PhaseSections + /// + private async Task> SearchPhases( + string searchQuery, + CancellationToken cancellationToken) + { + var phases = await _context.ProjectPhases + .Where(ph => + ph.Name.ToLower().Contains(searchQuery) || + (ph.Description != null && ph.Description.ToLower().Contains(searchQuery))) + .Select(ph => new ProjectHierarchySearchResultDto + { + Id = ph.Id, + Title = ph.Name, + Level = ProjectHierarchyLevel.Phase, + ProjectId = ph.ProjectId, + PhaseId = null + }) + .ToListAsync(cancellationToken); + + return phases; + } + + /// + /// جستجو در جدول تسک‌های پروژه (نام، توضیحات) و TaskSections (نام مهارت، توضیح اولیه، اطلاعات اضافی) + /// + private async Task> SearchTasks( + string searchQuery, + CancellationToken cancellationToken) + { + var tasks = await _context.ProjectTasks + .Include(t => t.Sections) + .Include(t => t.Phase) + .Where(t => + t.Name.ToLower().Contains(searchQuery) || + (t.Description != null && t.Description.ToLower().Contains(searchQuery)) || + t.Sections.Any(s => + (s.InitialDescription != null && s.InitialDescription.ToLower().Contains(searchQuery)) || + s.AdditionalTimes.Any(at => at.Reason != null && at.Reason.ToLower().Contains(searchQuery)))) + .Select(t => new ProjectHierarchySearchResultDto + { + Id = t.Id, + Title = t.Name, + Level = ProjectHierarchyLevel.Task, + ProjectId = t.Phase.ProjectId, + PhaseId = t.PhaseId + }) + .ToListAsync(cancellationToken); + + return tasks; + } + + /// + /// ترتیب سطح برای مرتب‌سازی: پروژه (1) → فاز (2) → تسک (3) + /// + private static int GetLevelOrder(ProjectHierarchyLevel level) + { + return level switch + { + ProjectHierarchyLevel.Project => 1, + ProjectHierarchyLevel.Phase => 2, + ProjectHierarchyLevel.Task => 3, + _ => 4 + }; + } +} + + diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryValidator.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryValidator.cs new file mode 100644 index 00000000..4a32fd92 --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; + +namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch; + +/// +/// اعتبارسنج برای درخواست جستجوی سراسری +/// +public class GetProjectHierarchySearchQueryValidator : AbstractValidator +{ + public GetProjectHierarchySearchQueryValidator() + { + RuleFor(x => x.SearchQuery) + .NotEmpty().WithMessage("متن جستجو نمی‌تواند خالی باشد.") + .MinimumLength(2).WithMessage("متن جستجو باید حداقل 2 حرف باشد.") + .MaximumLength(500).WithMessage("متن جستجو نمی‌تواند بیش از 500 حرف باشد."); + } +} + diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchResponse.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchResponse.cs new file mode 100644 index 00000000..b03fb96e --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchResponse.cs @@ -0,0 +1,8 @@ +namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch; + +/// +/// پوسته‌ی پاسخ برای نتایج جستجوی سراسری +/// +public record GetProjectHierarchySearchResponse( + List Results); + diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/ProjectHierarchySearchResultDto.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/ProjectHierarchySearchResultDto.cs new file mode 100644 index 00000000..8a8b70ff --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/ProjectHierarchySearchResultDto.cs @@ -0,0 +1,36 @@ +using GozareshgirProgramManager.Domain.ProjectAgg.Enums; + +namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch; + +/// +/// DTO برای نتایج جستجوی سراسری در سلسله‌مراتب پروژه. +/// حاوی اطلاعات کافی برای بازسازی مسیر سلسله‌مراتب و بسط درخت در رابط کاربری است. +/// +public record ProjectHierarchySearchResultDto +{ + /// + /// شناسه آیتم (پروژه، فاز یا تسک) + /// + public Guid Id { get; init; } + + /// + /// نام/عنوان آیتم + /// + public string Title { get; init; } = string.Empty; + + /// + /// سطح سلسله‌مراتب این آیتم + /// + public ProjectHierarchyLevel Level { get; init; } + + /// + /// شناسه پروژه - همیشه برای فاز و تسک پر شده است، برای پروژه با شناسه خود پر می‌شود + /// + public Guid ProjectId { get; init; } + + /// + /// شناسه فاز - فقط برای تسک پر شده است، برای پروژه و فاز خالی است + /// + public Guid? PhaseId { get; init; } +} + diff --git a/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs b/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs index e13bcc97..a2c21899 100644 --- a/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs +++ b/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using GozareshgirProgramManager.Application._Common.Models; using GozareshgirProgramManager.Application.Modules.Projects.Commands.AssignProject; using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoStopOverTimeTaskSections; @@ -17,6 +17,7 @@ using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectBoar using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardDetail; using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardList; using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails; +using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch; using MediatR; using Microsoft.AspNetCore.Mvc; using ServiceHost.BaseControllers; @@ -40,6 +41,15 @@ public class ProjectController : ProgramManagerBaseController return res; } + [HttpGet("search")] + public async Task>> Search( + [FromQuery] string query) + { + var searchQuery = new GetProjectHierarchySearchQuery(query); + var res = await _mediator.Send(searchQuery); + return res; + } + [HttpPost] public async Task> Create([FromBody] CreateProjectCommand command) { From c2fca9f9ebbb64bc7369cdad52ce8805879dce71 Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 4 Jan 2026 15:50:56 +0330 Subject: [PATCH 08/12] feat: rename project hierarchy search components and add validation for search query --- ...earchQuery.cs => GetProjectSearchQuery.cs} | 4 +-- ...ler.cs => GetProjectSearchQueryHandler.cs} | 31 ++++++------------- ...r.cs => GetProjectSearchQueryValidator.cs} | 4 +-- ...esponse.cs => GetProjectSearchResponse.cs} | 2 +- .../ProjectHierarchySearchResultDto.cs | 2 +- .../ProgramManager/ProjectController.cs | 4 +-- 6 files changed, 17 insertions(+), 30 deletions(-) rename ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/{GetProjectHierarchySearchQuery.cs => GetProjectSearchQuery.cs} (80%) rename ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/{GetProjectHierarchySearchQueryHandler.cs => GetProjectSearchQueryHandler.cs} (82%) rename ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/{GetProjectHierarchySearchQueryValidator.cs => GetProjectSearchQueryValidator.cs} (79%) rename ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/{GetProjectHierarchySearchResponse.cs => GetProjectSearchResponse.cs} (84%) diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQuery.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQuery.cs similarity index 80% rename from ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQuery.cs rename to ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQuery.cs index 22f6c800..8e9e292d 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQuery.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQuery.cs @@ -6,6 +6,6 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProj /// درخواست جستجو در سراسر سلسله‌مراتب پروژه (پروژه، فاز، تسک). /// نتایج با اطلاعات مسیر سلسله‌مراتب برای پشتیبانی از ناوبری درخت در رابط کاربری بازگردانده می‌شود. /// -public record GetProjectHierarchySearchQuery( - string SearchQuery) : IBaseQuery; +public record GetProjectSearchQuery( + string SearchQuery) : IBaseQuery; \ No newline at end of file diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQueryHandler.cs similarity index 82% rename from ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryHandler.cs rename to ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQueryHandler.cs index 2e20e5e6..e9cb3c25 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryHandler.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQueryHandler.cs @@ -10,18 +10,18 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProj /// این handler در تمام سطح‌های پروژه، فاز و تسک جستجو می‌کند و از تمام فیلدهای متنی (نام، توضیحات) استفاده می‌کند. /// همچنین در زیرمجموعه‌های هر سطح (ProjectSections، PhaseSections، TaskSections) جستجو می‌کند. /// -public class GetProjectHierarchySearchQueryHandler : IBaseQueryHandler +public class GetProjectSearchQueryHandler : IBaseQueryHandler { private readonly IProgramManagerDbContext _context; private const int MaxResults = 50; - public GetProjectHierarchySearchQueryHandler(IProgramManagerDbContext context) + public GetProjectSearchQueryHandler(IProgramManagerDbContext context) { _context = context; } - public async Task> Handle( - GetProjectHierarchySearchQuery request, + public async Task> Handle( + GetProjectSearchQuery request, CancellationToken cancellationToken) { var searchQuery = request.SearchQuery.ToLower(); @@ -41,13 +41,13 @@ public class GetProjectHierarchySearchQueryHandler : IBaseQueryHandler GetLevelOrder(r.Level)) + .OrderBy(r => r.Level) .ThenBy(r => r.Title) .Take(MaxResults) .ToList(); - var response = new GetProjectHierarchySearchResponse(sortedResults); - return OperationResult.Success(response); + var response = new GetProjectSearchResponse(sortedResults); + return OperationResult.Success(response); } /// @@ -66,7 +66,7 @@ public class GetProjectHierarchySearchQueryHandler : IBaseQueryHandler - /// ترتیب سطح برای مرتب‌سازی: پروژه (1) → فاز (2) → تسک (3) - /// - private static int GetLevelOrder(ProjectHierarchyLevel level) - { - return level switch - { - ProjectHierarchyLevel.Project => 1, - ProjectHierarchyLevel.Phase => 2, - ProjectHierarchyLevel.Task => 3, - _ => 4 - }; - } + } diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryValidator.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQueryValidator.cs similarity index 79% rename from ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryValidator.cs rename to ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQueryValidator.cs index 4a32fd92..70c25241 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchQueryValidator.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQueryValidator.cs @@ -5,9 +5,9 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProj /// /// اعتبارسنج برای درخواست جستجوی سراسری /// -public class GetProjectHierarchySearchQueryValidator : AbstractValidator +public class GetProjectSearchQueryValidator : AbstractValidator { - public GetProjectHierarchySearchQueryValidator() + public GetProjectSearchQueryValidator() { RuleFor(x => x.SearchQuery) .NotEmpty().WithMessage("متن جستجو نمی‌تواند خالی باشد.") diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchResponse.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchResponse.cs similarity index 84% rename from ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchResponse.cs rename to ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchResponse.cs index b03fb96e..288206f7 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectHierarchySearchResponse.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchResponse.cs @@ -3,6 +3,6 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProj /// /// پوسته‌ی پاسخ برای نتایج جستجوی سراسری /// -public record GetProjectHierarchySearchResponse( +public record GetProjectSearchResponse( List Results); diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/ProjectHierarchySearchResultDto.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/ProjectHierarchySearchResultDto.cs index 8a8b70ff..036d3a8b 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/ProjectHierarchySearchResultDto.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/ProjectHierarchySearchResultDto.cs @@ -26,7 +26,7 @@ public record ProjectHierarchySearchResultDto /// /// شناسه پروژه - همیشه برای فاز و تسک پر شده است، برای پروژه با شناسه خود پر می‌شود /// - public Guid ProjectId { get; init; } + public Guid? ProjectId { get; init; } /// /// شناسه فاز - فقط برای تسک پر شده است، برای پروژه و فاز خالی است diff --git a/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs b/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs index a2c21899..bf9617a5 100644 --- a/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs +++ b/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs @@ -42,10 +42,10 @@ public class ProjectController : ProgramManagerBaseController } [HttpGet("search")] - public async Task>> Search( + public async Task>> Search( [FromQuery] string query) { - var searchQuery = new GetProjectHierarchySearchQuery(query); + var searchQuery = new GetProjectSearchQuery(query); var res = await _mediator.Send(searchQuery); return res; } From 33833a408cc0c3eea0827c6dc242f72256ce622e Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 4 Jan 2026 16:58:36 +0330 Subject: [PATCH 09/12] feat: include PhaseSections in ProjectPhase retrieval for enhanced task data --- .../Persistence/Repositories/ProjectPhaseRepository.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/ProjectPhaseRepository.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/ProjectPhaseRepository.cs index 81306c82..f00d4512 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/ProjectPhaseRepository.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/ProjectPhaseRepository.cs @@ -19,6 +19,7 @@ public class ProjectPhaseRepository : RepositoryBase, IProje public Task GetWithTasksAsync(Guid phaseId) { return _context.ProjectPhases + .Include(x=>x.PhaseSections) .Include(p => p.Tasks) .ThenInclude(t => t.Sections) .ThenInclude(s => s.Skill) From abd221cb5595566a875bf0bd9d2176ef903003d5 Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 4 Jan 2026 17:18:36 +0330 Subject: [PATCH 10/12] feat: fix SkillName and SkillId assignment in ProjectSetTimeDetailsQueryHandler --- .../ProjectSetTimeDetailsQueryHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 c3f48bfa..aee31370 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 @@ -75,14 +75,14 @@ public class ProjectSetTimeDetailsQueryHandler CreationDate = x.CreationDate.ToFarsi() }).OrderBy(x => x.CreationDate).ToList() ?? [], InitCreationTime = section?.CreationDate.ToFarsi() ?? "", - SkillName = skill?.Name ?? "", + SkillName = skill.Name ?? "", UserFullName = user?.FullName ?? "", SectionId = section?.Id ?? Guid.Empty, InitialDescription = section?.InitialDescription ?? "", InitialHours = (int)(section?.InitialEstimatedHours.TotalHours ?? 0), InitialMinutes = section?.InitialEstimatedHours.Minutes ?? 0, UserId = section?.OriginalAssignedUserId ?? 0, - SkillId = task.Id, + SkillId = skill.Id, }; }).OrderBy(x => x.SkillId).ToList(), task.Id, From 00b5066f6fe6480c2331b74f7b4c56d7bff63de1 Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 4 Jan 2026 17:40:42 +0330 Subject: [PATCH 11/12] feat: implement removal of invalid project, phase, and task sections based on skill validation --- .../SetTimeProjectCommandHandler.cs | 64 ++++++++++++++++++- .../ProjectAgg/Entities/Project.cs | 9 +++ .../ProjectAgg/Entities/ProjectPhase.cs | 9 +++ 3 files changed, 81 insertions(+), 1 deletion(-) 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 e88116b6..89ae80a1 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 @@ -72,6 +72,17 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandlerx.UserId is > 0).ToList(); + // حذف ProjectSections که در validSkills نیستند + var validSkillIds = skillItems.Select(x => x.SkillId).ToList(); + var sectionsToRemove = project.ProjectSections + .Where(s => !validSkillIds.Contains(s.SkillId)) + .ToList(); + + foreach (var section in sectionsToRemove) + { + project.RemoveProjectSection(section.SkillId); + } + // تخصیص در سطح پروژه foreach (var item in skillItems) { @@ -102,6 +113,16 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler !validSkillIds.Contains(s.SkillId)) + .ToList(); + + foreach (var section in phaseSectionsToRemove) + { + phase.RemovePhaseSection(section.SkillId); + } + // برای phase هم باید section‌ها را به‌روزرسانی کنیم foreach (var item in skillItems ) { @@ -122,6 +143,16 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler !validSkillIds.Contains(s.SkillId)) + .ToList(); + + foreach (var section in taskSectionsToRemove) + { + task.RemoveSection(section.Id); + } + foreach (var item in skillItems) { var section = task.Sections.FirstOrDefault(s => s.SkillId == item.SkillId); @@ -177,6 +208,18 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandlerx.UserId is > 0).ToList(); + + // حذف PhaseSections که در validSkills نیستند + var validSkillIds = skillItems.Select(x => x.SkillId).ToList(); + var sectionsToRemove = phase.PhaseSections + .Where(s => !validSkillIds.Contains(s.SkillId)) + .ToList(); + + foreach (var section in sectionsToRemove) + { + phase.RemovePhaseSection(section.SkillId); + } + // به‌روزرسانی یا اضافه کردن PhaseSection foreach (var item in skillItems) { @@ -200,6 +243,16 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler !validSkillIds.Contains(s.SkillId)) + .ToList(); + + foreach (var section in taskSectionsToRemove) + { + task.RemoveSection(section.Id); + } + foreach (var item in skillItems) { var section = task.Sections.FirstOrDefault(s => s.SkillId == item.SkillId); @@ -246,7 +299,16 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandlerx.UserId is > 0).ToList(); - task.ClearTaskSections(); + // حذف سکشن‌هایی که در validSkills نیستند + var validSkillIds = validSkills.Select(x => x.SkillId).ToList(); + var sectionsToRemove = task.Sections + .Where(s => !validSkillIds.Contains(s.SkillId)) + .ToList(); + + foreach (var sectionToRemove in sectionsToRemove) + { + task.RemoveSection(sectionToRemove.Id); + } foreach (var skillItem in validSkills) { diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/Project.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/Project.cs index dc2bfa15..8e8e2b31 100644 --- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/Project.cs +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/Project.cs @@ -74,6 +74,15 @@ public class Project : ProjectHierarchyNode } } + public void RemoveProjectSection(Guid skillId) + { + var section = _projectSections.FirstOrDefault(s => s.SkillId == skillId); + if (section != null) + { + _projectSections.Remove(section); + } + } + public void ClearProjectSections() { _projectSections.Clear(); diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectPhase.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectPhase.cs index 3534c50f..60b98df9 100644 --- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectPhase.cs +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectPhase.cs @@ -87,6 +87,15 @@ public class ProjectPhase : ProjectHierarchyNode } } + public void RemovePhaseSection(Guid skillId) + { + var section = _phaseSections.FirstOrDefault(s => s.SkillId == skillId); + if (section != null) + { + _phaseSections.Remove(section); + } + } + public void ClearPhaseSections() { _phaseSections.Clear(); From 8d93fa4fc6d43c85ee5fe8e5bc146105268571bf Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 4 Jan 2026 20:39:20 +0330 Subject: [PATCH 12/12] Add approval workflow for task section completion - Updated status transitions to include PendingForCompletion before Completed - Added API endpoint and command/handler for approving/rejecting section completion - Fixed additional time calculation to include minutes - Refactored project board query logic and improved user ordering - Updated launch settings with new HTTPS endpoint - Documented progress percentage feature for TaskSection --- .../ApproveTaskSectionCompletionCommand.cs | 57 ++++++++ ...veTaskSectionCompletionCommandValidator.cs | 14 ++ .../ChangeStatusSectionCommandHandler.cs | 6 +- .../SetTimeProjectCommandHandler.cs | 2 +- .../ProjectBoardListQueryHandler.cs | 126 ++++++++++-------- .../ProgramManager/ProjectController.cs | 10 +- 6 files changed, 154 insertions(+), 61 deletions(-) create mode 100644 ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommand.cs create mode 100644 ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommandValidator.cs diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommand.cs new file mode 100644 index 00000000..4b21408b --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommand.cs @@ -0,0 +1,57 @@ +using GozareshgirProgramManager.Application._Common.Interfaces; +using GozareshgirProgramManager.Application._Common.Models; +using GozareshgirProgramManager.Domain._Common; +using GozareshgirProgramManager.Domain._Common.Exceptions; +using GozareshgirProgramManager.Domain.ProjectAgg.Enums; +using GozareshgirProgramManager.Domain.ProjectAgg.Repositories; + +namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion; + +public record ApproveTaskSectionCompletionCommand(Guid TaskSectionId, bool IsApproved) : IBaseCommand; + +public class ApproveTaskSectionCompletionCommandHandler : IBaseCommandHandler +{ + private readonly ITaskSectionRepository _taskSectionRepository; + private readonly IUnitOfWork _unitOfWork; + private readonly IAuthHelper _authHelper; + + public ApproveTaskSectionCompletionCommandHandler( + ITaskSectionRepository taskSectionRepository, + IUnitOfWork unitOfWork, + IAuthHelper authHelper) + { + _taskSectionRepository = taskSectionRepository; + _unitOfWork = unitOfWork; + _authHelper = authHelper; + } + + public async Task Handle(ApproveTaskSectionCompletionCommand request, CancellationToken cancellationToken) + { + var currentUserId = _authHelper.GetCurrentUserId() + ?? throw new UnAuthorizedException(" ? "); + + var section = await _taskSectionRepository.GetByIdAsync(request.TaskSectionId, cancellationToken); + if (section == null) + { + return OperationResult.NotFound(" ? "); + } + + if (section.Status != TaskSectionStatus.PendingForCompletion) + { + return OperationResult.Failure(" ԝ?? ʘ? ?? ? "); + } + + if (request.IsApproved) + { + section.UpdateStatus(TaskSectionStatus.Completed); + } + else + { + section.UpdateStatus(TaskSectionStatus.Incomplete); + } + + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Success(); + } +} diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommandValidator.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommandValidator.cs new file mode 100644 index 00000000..c12a43c3 --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; + +namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion; + +public class ApproveTaskSectionCompletionCommandValidator : AbstractValidator +{ + public ApproveTaskSectionCompletionCommandValidator() + { + RuleFor(c => c.TaskSectionId) + .NotEmpty() + .NotNull() + .WithMessage(" ? ? "); + } +} diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs index d5d0cd62..178a0257 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs @@ -89,9 +89,9 @@ public class ChangeStatusSectionCommandHandler : IBaseCommandHandler> { { TaskSectionStatus.ReadyToStart, [TaskSectionStatus.InProgress] }, - { TaskSectionStatus.InProgress, [TaskSectionStatus.Incomplete, TaskSectionStatus.Completed] }, - { TaskSectionStatus.Incomplete, [TaskSectionStatus.InProgress, TaskSectionStatus.Completed] }, - { TaskSectionStatus.Completed, [TaskSectionStatus.InProgress, TaskSectionStatus.Incomplete] }, // Can return to InProgress or Incomplete + { TaskSectionStatus.InProgress, [TaskSectionStatus.Incomplete, TaskSectionStatus.PendingForCompletion] }, + { TaskSectionStatus.Incomplete, [TaskSectionStatus.InProgress, TaskSectionStatus.PendingForCompletion] }, + { TaskSectionStatus.PendingForCompletion, [TaskSectionStatus.InProgress, TaskSectionStatus.Incomplete] }, // Can return to InProgress or Incomplete { TaskSectionStatus.NotAssigned, [TaskSectionStatus.InProgress, TaskSectionStatus.ReadyToStart] } }; 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 89ae80a1..2ee2ea7d 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 @@ -366,7 +366,7 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler x.Id, x => x.FullName, cancellationToken); - var result = data.Select(x => - { - // محاسبه یکبار برای هر Activity و Cache کردن نتیجه - var activityTimeData = x.Activities.Select(a => + var result = data + .Select(x => { - var timeSpent = a.GetTimeSpent(); - return new + // محاسبه یکبار برای هر Activity و Cache کردن نتیجه + var activityTimeData = x.Activities.Select(a => { - Activity = a, - TimeSpent = timeSpent, - TotalSeconds = timeSpent.TotalSeconds, - FormattedTime = timeSpent.ToString(@"hh\:mm") - }; - }).ToList(); - - // ادغام پشت سر هم فعالیت‌های یک کاربر - var mergedHistories = new List(); - foreach (var activityData in activityTimeData) - { - var lastHistory = mergedHistories.LastOrDefault(); - - // اگر آخرین history برای همین کاربر باشد، زمان‌ها را جمع می‌کنیم - if (lastHistory != null && lastHistory.UserId == activityData.Activity.UserId) - { - var totalTimeSpan = lastHistory.WorkedTimeSpan + activityData.TimeSpent; - lastHistory.WorkedTimeSpan = totalTimeSpan; - lastHistory.WorkedTime = totalTimeSpan.ToString(@"hh\:mm"); - } - else - { - // در غیر این صورت، یک history جدید اضافه می‌کنیم - mergedHistories.Add(new ProjectProgressHistoryDto() + var timeSpent = a.GetTimeSpent(); + return new { - UserId = activityData.Activity.UserId, - IsCurrentUser = activityData.Activity.UserId == currentUserId, - Name = users.GetValueOrDefault(activityData.Activity.UserId, "ناشناس"), - WorkedTime = activityData.FormattedTime, - WorkedTimeSpan = activityData.TimeSpent, - }); - } - } + Activity = a, + TimeSpent = timeSpent, + TotalSeconds = timeSpent.TotalSeconds, + FormattedTime = timeSpent.ToString(@"hh\:mm") + }; + }).ToList(); - return new ProjectBoardListResponse() - { - Id = x.Id, - PhaseName = x.Task.Phase.Name, - ProjectName = x.Task.Phase.Project.Name, - TaskName = x.Task.Name, - SectionStatus = x.Status, - Progress = new ProjectProgressDto() + // ادغام پشت سر هم فعالیت‌های یک کاربر + var mergedHistories = new List(); + foreach (var activityData in activityTimeData) { - CompleteSecond = x.FinalEstimatedHours.TotalSeconds, - CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds), - Histories = mergedHistories - }, - OriginalUser = users.GetValueOrDefault(x.OriginalAssignedUserId, "ناشناس"), - AssignedUser = x.CurrentAssignedUserId == x.OriginalAssignedUserId ? null - : users.GetValueOrDefault(x.CurrentAssignedUserId, "ناشناس"), - SkillName = x.Skill?.Name??"-", - }; - }).ToList(); + var lastHistory = mergedHistories.LastOrDefault(); + + // اگر آخرین history برای همین کاربر باشد، زمان‌ها را جمع می‌کنیم + if (lastHistory != null && lastHistory.UserId == activityData.Activity.UserId) + { + var totalTimeSpan = lastHistory.WorkedTimeSpan + activityData.TimeSpent; + lastHistory.WorkedTimeSpan = totalTimeSpan; + lastHistory.WorkedTime = totalTimeSpan.ToString(@"hh\:mm"); + } + else + { + // در غیر این صورت، یک history جدید اضافه می‌کنیم + mergedHistories.Add(new ProjectProgressHistoryDto() + { + UserId = activityData.Activity.UserId, + IsCurrentUser = activityData.Activity.UserId == currentUserId, + Name = users.GetValueOrDefault(activityData.Activity.UserId, "ناشناس"), + WorkedTime = activityData.FormattedTime, + WorkedTimeSpan = activityData.TimeSpent, + }); + } + } + + mergedHistories = mergedHistories.OrderByDescending(h => h.IsCurrentUser).ToList(); + + return new ProjectBoardListResponse() + { + Id = x.Id, + PhaseName = x.Task.Phase.Name, + ProjectName = x.Task.Phase.Project.Name, + TaskName = x.Task.Name, + SectionStatus = x.Status, + Progress = new ProjectProgressDto() + { + CompleteSecond = x.FinalEstimatedHours.TotalSeconds, + CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds), + Histories = mergedHistories + }, + OriginalUser = users.GetValueOrDefault(x.OriginalAssignedUserId, "ناشناس"), + AssignedUser = x.CurrentAssignedUserId == x.OriginalAssignedUserId ? null + : users.GetValueOrDefault(x.CurrentAssignedUserId, "ناشناس"), + SkillName = x.Skill?.Name??"-", + }; + }) + .OrderByDescending(r => + { + // اگر AssignedUser null نباشد، بررسی کن که برابر current user هست یا نه + if (r.AssignedUser != null) + { + return users.FirstOrDefault(u => u.Value == r.AssignedUser).Key == currentUserId; + } + // اگر AssignedUser null بود، از OriginalUser بررسی کن + return users.FirstOrDefault(u => u.Value == r.OriginalUser).Key == currentUserId; + }) + .ToList(); return OperationResult>.Success(result); } diff --git a/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs b/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs index bf9617a5..153ca3cd 100644 --- a/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs +++ b/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs @@ -1,5 +1,6 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using GozareshgirProgramManager.Application._Common.Models; +using GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion; using GozareshgirProgramManager.Application.Modules.Projects.Commands.AssignProject; using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoStopOverTimeTaskSections; using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoUpdateDeployStatus; @@ -157,4 +158,11 @@ public class ProjectController : ProgramManagerBaseController var res = await _mediator.Send(command); return res; } + + [HttpPost("approve-completion")] + public async Task> ApproveTaskSectionCompletion([FromBody] ApproveTaskSectionCompletionCommand command) + { + var res = await _mediator.Send(command); + return res; + } } \ No newline at end of file