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 7066148e..91a82c28 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 @@ -1,20 +1,28 @@ using GozareshgirProgramManager.Domain.ProjectAgg.Enums; namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList; -public record GetProjectListDto + +// Base DTO shared across project, phase, and task +public class GetProjectItemDto { public Guid Id { get; init; } public string Name { get; init; } = string.Empty; public int Percentage { get; init; } public ProjectHierarchyLevel Level { get; init; } public Guid? ParentId { get; init; } - 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; } - + public AssignmentStatus Front { get; set; } + public AssignmentStatus Backend { get; set; } + public AssignmentStatus Design { get; set; } } +// Project DTO (no extra fields; inherits from base) +public class GetProjectDto : GetProjectItemDto +{ +} - +// Phase DTO (no extra fields; inherits from base) +public class GetPhaseDto : GetProjectItemDto +{ +} 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 621ef025..d26a06b1 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 @@ -3,6 +3,7 @@ using GozareshgirProgramManager.Application._Common.Models; using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using Microsoft.EntityFrameworkCore; +using System.Linq; namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList; @@ -17,47 +18,47 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler> Handle(GetProjectsListQuery request, CancellationToken cancellationToken) { - List projects; + var projects = new List(); + var phases = new List(); + var tasks = new List(); switch (request.HierarchyLevel) { case ProjectHierarchyLevel.Project: projects = await GetProjects(request.ParentId, cancellationToken); + await SetSkillFlags(projects, cancellationToken); break; case ProjectHierarchyLevel.Phase: - projects = await GetPhases(request.ParentId, cancellationToken); + phases = await GetPhases(request.ParentId, cancellationToken); + await SetSkillFlags(phases, cancellationToken); break; case ProjectHierarchyLevel.Task: - projects = await GetTasks(request.ParentId, cancellationToken); + tasks = await GetTasks(request.ParentId, cancellationToken); + // Tasks don't need SetSkillFlags because they have Sections list break; default: return OperationResult.Failure("سطح سلسله مراتب نامعتبر است"); } - await SetSkillFlags(projects, cancellationToken); - var response = new GetProjectsListResponse(projects); + var response = new GetProjectsListResponse(projects, phases, tasks); return OperationResult.Success(response); } - private async Task> GetProjects(Guid? parentId, CancellationToken cancellationToken) + private async Task> GetProjects(Guid? parentId, CancellationToken cancellationToken) { var query = _context.Projects.AsQueryable(); - - // پروژه‌ها سطح بالا هستند و parentId ندارند، فقط در صورت null بودن parentId نمایش داده می‌شوند if (parentId.HasValue) { - return new List(); // پروژه‌ها parent ندارند + return new List(); } - - var projects = await query + var entities = await query .OrderByDescending(p => p.CreationDate) .ToListAsync(cancellationToken); - var result = new List(); - - foreach (var project in projects) + var result = new List(); + foreach (var project in entities) { var (percentage, totalTime) = await CalculateProjectPercentage(project, cancellationToken); - result.Add(new GetProjectListDto + result.Add(new GetProjectDto { Id = project.Id, Name = project.Name, @@ -68,28 +69,24 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler> GetPhases(Guid? parentId, CancellationToken cancellationToken) + private async Task> GetPhases(Guid? parentId, CancellationToken cancellationToken) { var query = _context.ProjectPhases.AsQueryable(); - if (parentId.HasValue) { query = query.Where(x => x.ProjectId == parentId); } - - var phases = await query + var entities = await query .OrderByDescending(p => p.CreationDate) .ToListAsync(cancellationToken); - var result = new List(); - - foreach (var phase in phases) + var result = new List(); + foreach (var phase in entities) { var (percentage, totalTime) = await CalculatePhasePercentage(phase, cancellationToken); - result.Add(new GetProjectListDto + result.Add(new GetPhaseDto { Id = phase.Id, Name = phase.Name, @@ -100,28 +97,87 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler> GetTasks(Guid? parentId, CancellationToken cancellationToken) + private async Task> GetTasks(Guid? parentId, CancellationToken cancellationToken) { var query = _context.ProjectTasks.AsQueryable(); - if (parentId.HasValue) { query = query.Where(x => x.PhaseId == parentId); } - - var tasks = await query + var entities = await query .OrderByDescending(t => t.CreationDate) .ToListAsync(cancellationToken); - var result = new List(); + var result = new List(); + // دریافت تمام Skills + var allSkills = await _context.Skills + .Select(s => new { s.Id, s.Name }) + .ToListAsync(cancellationToken); - foreach (var task in tasks) + foreach (var task in entities) { var (percentage, totalTime) = await CalculateTaskPercentage(task, cancellationToken); - result.Add(new GetProjectListDto + var sections = await _context.TaskSections + .Include(s => s.Activities) + .Include(s => s.Skill) + .Where(s => s.TaskId == task.Id) + .ToListAsync(cancellationToken); + + // جمع‌آوری تمام userId های مورد نیاز + var userIds = sections + .Where(s => s.CurrentAssignedUserId > 0) + .Select(s => s.CurrentAssignedUserId) + .Distinct() + .ToList(); + + // دریافت اطلاعات کاربران + var users = await _context.Users + .Where(u => userIds.Contains(u.Id)) + .Select(u => new { u.Id, u.FullName }) + .ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken); + + // محاسبه SpentTime و RemainingTime + var spentTime = TimeSpan.FromTicks(sections.Sum(s => s.Activities.Sum(a => a.GetTimeSpent().Ticks))); + var remainingTime = totalTime - spentTime; + + // ساخت section DTOs برای تمام Skills + var sectionDtos = allSkills.Select(skill => + { + var section = sections.FirstOrDefault(s => s.SkillId == skill.Id); + + if (section == null) + { + // اگر section وجود نداشت، یک DTO با وضعیت Unassigned برمی‌گردانیم + return new GetTaskSectionDto + { + Id = Guid.Empty, + SkillName = skill.Name ?? string.Empty, + SpentTime = TimeSpan.Zero, + TotalTime = TimeSpan.Zero, + Percentage = 0, + UserFullName = string.Empty, + AssignmentStatus = AssignmentStatus.Unassigned + }; + } + + // اگر section وجود داشت + return new GetTaskSectionDto + { + Id = section.Id, + SkillName = skill.Name ?? string.Empty, + SpentTime = TimeSpan.FromTicks(section.Activities.Sum(a => a.GetTimeSpent().Ticks)), + TotalTime = section.FinalEstimatedHours, + Percentage = (int)section.GetProgressPercentage(), + UserFullName = section.CurrentAssignedUserId > 0 && users.ContainsKey(section.CurrentAssignedUserId) + ? users[section.CurrentAssignedUserId] + : string.Empty, + AssignmentStatus = GetAssignmentStatus(section) + }; + }).ToList(); + + result.Add(new GetTaskDto { Id = task.Id, Name = task.Name, @@ -129,167 +185,144 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler projects, CancellationToken cancellationToken) + private async Task SetSkillFlags(List items, CancellationToken cancellationToken) where TItem : GetProjectItemDto { - if (!projects.Any()) + if (!items.Any()) return; - - var projectIds = projects.Select(x => x.Id).ToList(); - var hierarchyLevel = projects.First().Level; - + var ids = items.Select(x => x.Id).ToList(); + var hierarchyLevel = items.First().Level; switch (hierarchyLevel) { case ProjectHierarchyLevel.Project: - await SetSkillFlagsForProjects(projects, projectIds, cancellationToken); + await SetSkillFlagsForProjects(items, ids, cancellationToken); break; - case ProjectHierarchyLevel.Phase: - await SetSkillFlagsForPhases(projects, projectIds, cancellationToken); - break; - - case ProjectHierarchyLevel.Task: - await SetSkillFlagsForTasks(projects, projectIds, cancellationToken); + await SetSkillFlagsForPhases(items, ids, cancellationToken); break; } } - private async Task SetSkillFlagsForProjects(List projects, List projectIds, CancellationToken cancellationToken) + + private async Task SetSkillFlagsForProjects(List items, List projectIds, CancellationToken cancellationToken) where TItem : GetProjectItemDto { - var projectSections = await _context.ProjectSections - .Include(x => x.Skill) - .Where(s => projectIds.Contains(s.ProjectId)) + // For projects: gather all phases, then tasks, then sections + var phases = await _context.ProjectPhases + .Where(ph => projectIds.Contains(ph.ProjectId)) + .Select(ph => ph.Id) + .ToListAsync(cancellationToken); + var tasks = await _context.ProjectTasks + .Where(t => phases.Contains(t.PhaseId)) + .Select(t => t.Id) + .ToListAsync(cancellationToken); + var sections = await _context.TaskSections + .Include(s => s.Skill) + .Where(s => tasks.Contains(s.TaskId)) .ToListAsync(cancellationToken); - if (!projectSections.Any()) - return; - - foreach (var project in projects) + foreach (var item in items) { - var sections = projectSections.Where(s => s.ProjectId == project.Id).ToList(); - project.HasBackend = sections.Any(x => x.Skill?.Name == "Backend"); - project.HasFront = sections.Any(x => x.Skill?.Name == "Frontend"); - project.HasDesign = sections.Any(x => x.Skill?.Name == "UI/UX Design"); + var relatedPhases = phases; // used for filtering tasks by project + var relatedTasks = await _context.ProjectTasks + .Where(t => t.PhaseId != Guid.Empty && relatedPhases.Contains(t.PhaseId)) + .Select(t => t.Id) + .ToListAsync(cancellationToken); + var itemSections = sections.Where(s => relatedTasks.Contains(s.TaskId)); + item.Backend = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "Backend")); + item.Front = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "Frontend")); + item.Design = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "UI/UX Design")); } } - private async Task SetSkillFlagsForPhases(List projects, List phaseIds, CancellationToken cancellationToken) + private async Task SetSkillFlagsForPhases(List items, List phaseIds, CancellationToken cancellationToken) where TItem : GetProjectItemDto { - var phaseSections = await _context.PhaseSections - .Include(x => x.Skill) - .Where(s => phaseIds.Contains(s.PhaseId)) + // For phases: gather tasks, then sections + var tasks = await _context.ProjectTasks + .Where(t => phaseIds.Contains(t.PhaseId)) + .Select(t => t.Id) + .ToListAsync(cancellationToken); + var sections = await _context.TaskSections + .Include(s => s.Skill) + .Where(s => tasks.Contains(s.TaskId)) .ToListAsync(cancellationToken); - if (!phaseSections.Any()) - return; - - foreach (var phase in projects) + foreach (var item in items) { - var sections = phaseSections.Where(s => s.PhaseId == phase.Id).ToList(); - phase.HasBackend = sections.Any(x => x.Skill?.Name == "Backend"); - phase.HasFront = sections.Any(x => x.Skill?.Name == "Frontend"); - phase.HasDesign = sections.Any(x => x.Skill?.Name == "UI/UX Design"); - } - } - - private async Task SetSkillFlagsForTasks(List projects, List taskIds, CancellationToken cancellationToken) - { - var taskSections = await _context.TaskSections - .Include(x => x.Skill) - .Where(s => taskIds.Contains(s.TaskId)) - .ToListAsync(cancellationToken); - - if (!taskSections.Any()) - return; - - foreach (var task in projects) - { - var sections = taskSections.Where(s => s.TaskId == task.Id).ToList(); - task.HasBackend = sections.Any(x => x.Skill?.Name == "Backend"); - task.HasFront = sections.Any(x => x.Skill?.Name == "Frontend"); - task.HasDesign = sections.Any(x => x.Skill?.Name == "UI/UX Design"); + // Filter tasks for this phase + var phaseTaskIds = await _context.ProjectTasks + .Where(t => t.PhaseId == item.Id) + .Select(t => t.Id) + .ToListAsync(cancellationToken); + var itemSections = sections.Where(s => phaseTaskIds.Contains(s.TaskId)); + item.Backend = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "Backend")); + item.Front = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "Frontend")); + item.Design = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "UI/UX Design")); } } private async Task<(int Percentage, TimeSpan TotalTime)> CalculateProjectPercentage(Project project, CancellationToken cancellationToken) { - // گرفتن تمام فازهای پروژه var phases = await _context.ProjectPhases .Where(ph => ph.ProjectId == project.Id) .ToListAsync(cancellationToken); - if (!phases.Any()) return (0, TimeSpan.Zero); - - // محاسبه درصد هر فاز و میانگین‌گیری var phasePercentages = new List(); var totalTime = TimeSpan.Zero; - foreach (var phase in phases) { var (phasePercentage, phaseTime) = await CalculatePhasePercentage(phase, cancellationToken); phasePercentages.Add(phasePercentage); totalTime += phaseTime; } - var averagePercentage = phasePercentages.Any() ? (int)phasePercentages.Average() : 0; return (averagePercentage, totalTime); } private async Task<(int Percentage, TimeSpan TotalTime)> CalculatePhasePercentage(ProjectPhase phase, CancellationToken cancellationToken) { - // گرفتن تمام تسک‌های فاز var tasks = await _context.ProjectTasks .Where(t => t.PhaseId == phase.Id) .ToListAsync(cancellationToken); - if (!tasks.Any()) return (0, TimeSpan.Zero); - - // محاسبه درصد هر تسک و میانگین‌گیری var taskPercentages = new List(); var totalTime = TimeSpan.Zero; - foreach (var task in tasks) { var (taskPercentage, taskTime) = await CalculateTaskPercentage(task, cancellationToken); taskPercentages.Add(taskPercentage); totalTime += taskTime; } - var averagePercentage = taskPercentages.Any() ? (int)taskPercentages.Average() : 0; return (averagePercentage, totalTime); } private async Task<(int Percentage, TimeSpan TotalTime)> CalculateTaskPercentage(ProjectTask task, CancellationToken cancellationToken) { - // گرفتن تمام سکشن‌های تسک با activities var sections = await _context.TaskSections .Include(s => s.Activities) .Include(x=>x.AdditionalTimes) .Where(s => s.TaskId == task.Id) .ToListAsync(cancellationToken); - if (!sections.Any()) return (0, TimeSpan.Zero); - - // محاسبه درصد هر سکشن و میانگین‌گیری var sectionPercentages = new List(); var totalTime = TimeSpan.Zero; - foreach (var section in sections) { var (sectionPercentage, sectionTime) = CalculateSectionPercentage(section); sectionPercentages.Add(sectionPercentage); totalTime += sectionTime; } - var averagePercentage = sectionPercentages.Any() ? (int)sectionPercentages.Average() : 0; return (averagePercentage, totalTime); } @@ -298,5 +331,29 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler 0; + + // بررسی وجود time (InitialEstimatedHours بزرگتر از صفر باشد) + bool hasTime = section.InitialEstimatedHours > TimeSpan.Zero; + + // تعیین تکلیف شده: هم user و هم time تعیین شده + if (hasUser && hasTime) + return AssignmentStatus.Assigned; + + // فقط کاربر تعیین شده: user دارد ولی time ندارد + if (hasUser && !hasTime) + return AssignmentStatus.UserOnly; + + // تعیین تکلیف نشده: نه user دارد نه time + return AssignmentStatus.Unassigned; + } } diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListResponse.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListResponse.cs index 928501dd..f4934501 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListResponse.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListResponse.cs @@ -1,5 +1,6 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList; public record GetProjectsListResponse( - List Projects); - + List Projects, + List Phases, + List Tasks); diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetTaskListDto.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetTaskListDto.cs new file mode 100644 index 00000000..d48493d7 --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetTaskListDto.cs @@ -0,0 +1,31 @@ +using GozareshgirProgramManager.Domain.ProjectAgg.Enums; + +namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList; + +public class GetTaskDto +{ + public Guid Id { get; init; } + public string Name { get; init; } = string.Empty; + public int Percentage { get; init; } + public ProjectHierarchyLevel Level { get; init; } + public Guid? ParentId { get; init; } + public int TotalHours { get; set; } + public int Minutes { get; set; } + + // Task-specific fields + public TimeSpan SpentTime { get; init; } + public TimeSpan RemainingTime { get; init; } + public List Sections { get; init; } +} + +public class GetTaskSectionDto +{ + public Guid Id { get; init; } + public string SkillName { get; init; } = string.Empty; + public TimeSpan SpentTime { get; init; } + public TimeSpan TotalTime { get; init; } + public int Percentage { get; init; } + public string UserFullName{ get; init; } = string.Empty; + public AssignmentStatus AssignmentStatus { get; set; } + +} diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/AssignmentStatus.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/AssignmentStatus.cs new file mode 100644 index 00000000..91cbeffd --- /dev/null +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/AssignmentStatus.cs @@ -0,0 +1,23 @@ +namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums; + +/// +/// وضعیت تکلیف دهی برای بخش‌های مختلف پروژه +/// +public enum AssignmentStatus +{ + /// + /// تعیین تکلیف نشده + /// + Unassigned = 0, + + /// + /// تعیین تکلیف شده + /// + Assigned = 1, + + /// + /// فقط کاربر تعیین شده + /// + UserOnly = 2, +} + diff --git a/ServiceHost/Program.cs b/ServiceHost/Program.cs index 73ff3d88..012f1a0a 100644 --- a/ServiceHost/Program.cs +++ b/ServiceHost/Program.cs @@ -380,13 +380,7 @@ builder.Host.UseSerilog((context, services, configuration) => .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext(); - - // در محیط Development، EF Core Commands را هم لاگ می‌کنیم - if (context.HostingEnvironment.IsDevelopment()) - { - logConfig - .MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Information); - } + logConfig.WriteTo.File( path: Path.Combine(logDirectory, "gozareshgir_log.txt"),