Merge branch 'master' into Main

This commit is contained in:
2026-01-07 10:25:52 +03:30
14 changed files with 497 additions and 358 deletions

View File

@@ -89,6 +89,7 @@ public class ProjectSectionDto
public TimeSpan FinalEstimatedHours { get; set; }
public TimeSpan TotalTimeSpent { get; set; }
public double ProgressPercentage { get; set; }
public bool IsCompleted { get; set; }
public bool IsInProgress { get; set; }

View File

@@ -166,6 +166,7 @@ public static class ProjectMappingExtensions
CreationDate = section.CreationDate,
FinalEstimatedHours = section.FinalEstimatedHours,
TotalTimeSpent = section.GetTotalTimeSpent(),
ProgressPercentage = section.GetProgressPercentage(),
IsCompleted = section.IsCompleted(),
IsInProgress = section.IsInProgress(),
Activities = section.Activities.Select(a => a.ToDto()).ToList(),
@@ -188,6 +189,7 @@ public static class ProjectMappingExtensions
CreationDate = section.CreationDate,
FinalEstimatedHours = section.FinalEstimatedHours,
TotalTimeSpent = section.GetTotalTimeSpent(),
ProgressPercentage = section.GetProgressPercentage(),
IsCompleted = section.IsCompleted(),
IsInProgress = section.IsInProgress()
// No activities or additional times for summary

View File

@@ -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
{
}

View File

@@ -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<GetProjectsListQuer
public async Task<OperationResult<GetProjectsListResponse>> Handle(GetProjectsListQuery request, CancellationToken cancellationToken)
{
List<GetProjectListDto> projects;
var projects = new List<GetProjectDto>();
var phases = new List<GetPhaseDto>();
var tasks = new List<GetTaskDto>();
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<GetProjectsListResponse>.Failure("سطح سلسله مراتب نامعتبر است");
}
await SetSkillFlags(projects, cancellationToken);
var response = new GetProjectsListResponse(projects);
var response = new GetProjectsListResponse(projects, phases, tasks);
return OperationResult<GetProjectsListResponse>.Success(response);
}
private async Task<List<GetProjectListDto>> GetProjects(Guid? parentId, CancellationToken cancellationToken)
private async Task<List<GetProjectDto>> GetProjects(Guid? parentId, CancellationToken cancellationToken)
{
var query = _context.Projects.AsQueryable();
// پروژه‌ها سطح بالا هستند و parentId ندارند، فقط در صورت null بودن parentId نمایش داده می‌شوند
if (parentId.HasValue)
{
return new List<GetProjectListDto>(); // پروژه‌ها parent ندارند
return new List<GetProjectDto>();
}
var projects = await query
var entities = await query
.OrderByDescending(p => p.CreationDate)
.ToListAsync(cancellationToken);
var result = new List<GetProjectListDto>();
foreach (var project in projects)
var result = new List<GetProjectDto>();
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<GetProjectsListQuer
Minutes = totalTime.Minutes,
});
}
return result;
}
private async Task<List<GetProjectListDto>> GetPhases(Guid? parentId, CancellationToken cancellationToken)
private async Task<List<GetPhaseDto>> 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<GetProjectListDto>();
foreach (var phase in phases)
var result = new List<GetPhaseDto>();
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<GetProjectsListQuer
Minutes = totalTime.Minutes,
});
}
return result;
}
private async Task<List<GetProjectListDto>> GetTasks(Guid? parentId, CancellationToken cancellationToken)
private async Task<List<GetTaskDto>> 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<GetProjectListDto>();
var result = new List<GetTaskDto>();
// دریافت تمام 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,187 +185,175 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
ParentId = task.PhaseId,
Percentage = percentage,
TotalHours = (int)totalTime.TotalHours,
Minutes = totalTime.Minutes
Minutes = totalTime.Minutes,
SpentTime = spentTime,
RemainingTime = remainingTime,
Sections = sectionDtos,
});
}
return result;
}
private async Task SetSkillFlags(List<GetProjectListDto> projects, CancellationToken cancellationToken)
private async Task SetSkillFlags<TItem>(List<TItem> 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<GetProjectListDto> projects, List<Guid> projectIds, CancellationToken cancellationToken)
private async Task SetSkillFlagsForProjects<TItem>(List<TItem> items, List<Guid> 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<GetProjectListDto> projects, List<Guid> phaseIds, CancellationToken cancellationToken)
private async Task SetSkillFlagsForPhases<TItem>(List<TItem> items, List<Guid> 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<GetProjectListDto> projects, List<Guid> 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<int>();
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<int>();
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<int>();
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);
}
private static (int Percentage, TimeSpan TotalTime) CalculateSectionPercentage(TaskSection section)
{
// محاسبه کل زمان تخمین زده شده (اولیه + اضافی)
var totalEstimatedHours = section.FinalEstimatedHours.TotalHours;
return ((int)section.GetProgressPercentage(),section.FinalEstimatedHours);
}
// محاسبه کل زمان صرف شده از activities
var totalSpentTime = TimeSpan.FromHours(section.Activities.Sum(a => a.GetTimeSpent().TotalHours));
private static AssignmentStatus GetAssignmentStatus(TaskSection? section)
{
// تعیین تکلیف نشده: section وجود ندارد
if (section == null)
return AssignmentStatus.Unassigned;
if (totalEstimatedHours <= 0)
return (0, section.FinalEstimatedHours);
// بررسی وجود user
bool hasUser = section.CurrentAssignedUserId > 0;
// بررسی وجود time (InitialEstimatedHours بزرگتر از صفر باشد)
bool hasTime = section.InitialEstimatedHours > TimeSpan.Zero;
var totalSpentHours = totalSpentTime.TotalHours;
// تعیین تکلیف شده: هم user و هم time تعیین شده
if (hasUser && hasTime)
return AssignmentStatus.Assigned;
// محاسبه درصد (حداکثر 100%)
var percentage = (totalSpentHours / totalEstimatedHours) * 100;
return (Math.Min((int)Math.Round(percentage), 100), section.FinalEstimatedHours);
// فقط کاربر تعیین شده: user دارد ولی time ندارد
if (hasUser && !hasTime)
return AssignmentStatus.UserOnly;
// تعیین تکلیف نشده: نه user دارد نه time
return AssignmentStatus.Unassigned;
}
}

View File

@@ -1,5 +1,6 @@
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
public record GetProjectsListResponse(
List<GetProjectListDto> Projects);
List<GetProjectDto> Projects,
List<GetPhaseDto> Phases,
List<GetTaskDto> Tasks);

View File

@@ -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<GetTaskSectionDto> 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; }
}

View File

@@ -106,6 +106,7 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
Progress = new ProjectProgressDto()
{
CompleteSecond = x.FinalEstimatedHours.TotalSeconds,
Percentage = (int)x.GetProgressPercentage(),
CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds),
Histories = mergedHistories
},

View File

@@ -18,6 +18,7 @@ public class ProjectBoardListResponse
public class ProjectProgressDto
{
public double CurrentSecond { get; set; }
public int Percentage { get; set; }
public double CompleteSecond { get; set; }
public List<ProjectProgressHistoryDto> Histories { get; set; }
}

View File

@@ -12,14 +12,16 @@ public record ProjectDeployBoardDetailsResponse(
public record ProjectDeployBoardDetailPhaseItem(
string Name,
TimeSpan TotalTimeSpan,
TimeSpan DoneTimeSpan);
TimeSpan DoneTimeSpan,
int Percentage);
public record ProjectDeployBoardDetailTaskItem(
string Name,
TimeSpan TotalTimeSpan,
TimeSpan DoneTimeSpan,
int Percentage,
List<ProjectDeployBoardDetailItemSkill> Skills)
: ProjectDeployBoardDetailPhaseItem(Name, TotalTimeSpan, DoneTimeSpan);
: ProjectDeployBoardDetailPhaseItem(Name, TotalTimeSpan, DoneTimeSpan,Percentage);
public record ProjectDeployBoardDetailItemSkill(string OriginalUserFullName, string SkillName, int TimePercentage);
@@ -79,22 +81,20 @@ public class
var skillName = s.Skill?.Name ?? "بدون مهارت";
var totalTimeSpent = s.GetTotalTimeSpent();
var timePercentage = s.FinalEstimatedHours.Ticks > 0
? (int)((totalTimeSpent.Ticks / (double)s.FinalEstimatedHours.Ticks) * 100)
: 0;
var timePercentage = (int)s.GetProgressPercentage();
return new ProjectDeployBoardDetailItemSkill(
originalUserFullName,
skillName,
timePercentage);
}).ToList();
var taskPercentage = (int)Math.Round(skills.Average(x => x.TimePercentage));
return new ProjectDeployBoardDetailTaskItem(
t.Name,
totalTime,
doneTime,
taskPercentage,
skills);
}).ToList();
@@ -104,7 +104,10 @@ public class
var doneTimeSpan = tasksRes.Aggregate(TimeSpan.Zero,
(sum, next) => sum.Add(next.DoneTimeSpan));
var phaseRes = new ProjectDeployBoardDetailPhaseItem(phase.Name, totalTimeSpan, doneTimeSpan);
var phasePercentage = tasksRes.Average(x => x.Percentage);
var phaseRes = new ProjectDeployBoardDetailPhaseItem(phase.Name, totalTimeSpan, doneTimeSpan,
(int)phasePercentage);
var res = new ProjectDeployBoardDetailsResponse(phaseRes, tasksRes);

View File

@@ -17,6 +17,7 @@ public record ProjectDeployBoardListItem()
public int DoneTasks { get; set; }
public TimeSpan TotalTimeSpan { get; set; }
public TimeSpan DoneTimeSpan { get; set; }
public int Percentage { get; set; }
public ProjectDeployStatus DeployStatus { get; set; }
}
public record GetProjectsDeployBoardListResponse(List<ProjectDeployBoardListItem> Items);
@@ -66,7 +67,8 @@ public class ProjectDeployBoardListQueryHandler:IBaseQueryHandler<GetProjectDepl
.Select(x => x.TaskId).Distinct().Count(),
TotalTimeSpan = TimeSpan.FromTicks(g.Sum(x => x.InitialEstimatedHours.Ticks)),
DoneTimeSpan = TimeSpan.FromTicks(g.Sum(x=>x.GetTotalTimeSpent().Ticks)),
DeployStatus = g.First().Task.Phase.DeployStatus
DeployStatus = g.First().Task.Phase.DeployStatus,
Percentage = (int)Math.Round(g.Average(x => x.GetProgressPercentage()))
}).ToList();
var response = new GetProjectsDeployBoardListResponse(list);
return OperationResult<GetProjectsDeployBoardListResponse>.Success(response);

View File

@@ -157,6 +157,27 @@ public class TaskSection : EntityBase<Guid>
return TimeSpan.FromTicks(_activities.Sum(a => a.GetTimeSpent().Ticks));
}
/// <summary>
/// محاسبه درصد پیشرفت بر اساس زمان مصرف شده به تایم برآورد شده
/// اگر وضعیت Completed باشد، همیشه 100 درصد برمی‌گرداند
/// </summary>
public double GetProgressPercentage()
{
// اگر تسک کامل شده، همیشه 100 درصد
if (Status == TaskSectionStatus.Completed)
return 100.0;
// اگر تایم برآورد شده صفر است، درصد صفر است
if (FinalEstimatedHours.TotalHours <= 0)
return 0.0;
var timeSpent = GetTotalTimeSpent();
var percentage = (timeSpent.TotalMinutes / FinalEstimatedHours.TotalMinutes) * 100.0;
// محدود کردن درصد به 100 (در صورتی که زمان مصرف شده بیشتر از تخمین باشد)
return Math.Min(percentage, 100.0);
}
public bool IsCompleted()
{
return Status == TaskSectionStatus.Completed;

View File

@@ -0,0 +1,23 @@
namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
/// <summary>
/// وضعیت تکلیف دهی برای بخش‌های مختلف پروژه
/// </summary>
public enum AssignmentStatus
{
/// <summary>
/// تعیین تکلیف نشده
/// </summary>
Unassigned = 0,
/// <summary>
/// تعیین تکلیف شده
/// </summary>
Assigned = 1,
/// <summary>
/// فقط کاربر تعیین شده
/// </summary>
UserOnly = 2,
}