Compare commits

...

11 Commits

Author SHA1 Message Date
aa37ca4b28 add: enhance deployment status validation in ChangeDeployStatusProject command and include Activities in ProjectDeployBoardList query 2025-12-30 17:19:59 +03:30
45615684ed add: enhance skill calculation in ProjectDeployBoardDetailsQueryHandler and refine query conditions in ProjectDeployBoardListQueryHandler 2025-12-30 15:55:57 +03:30
94955ea1b4 add: update GetProjectDeployBoardDetails method to use ProjectDeployBoardDetailsQuery 2025-12-30 12:54:53 +03:30
656bb49fab add: remove GetByIdAsync method and add Id property to ProjectDeployBoardListItem 2025-12-30 12:30:06 +03:30
fb5b98bf25 add: update UpdateDeployStatus method to archive project phase upon deployment 2025-12-30 11:54:56 +03:30
1e733f3f20 add: implement ChangeDeployStatusProject command and handler for updating project deployment status 2025-12-30 11:10:40 +03:30
d855684cd7 add: implement AutoUpdateDeployStatusCommand for automatic deployment status updates 2025-12-29 12:16:51 +03:30
9e5e8d8e5d refactor: improve code formatting and structure in ProjectController 2025-12-29 11:45:32 +03:30
9eefdd8fd1 add: implement GetProjectDeployBoardDetails endpoint for retrieving project board details 2025-12-29 10:27:23 +03:30
2159901614 add: implement ProjectDeployBoardDetailsQuery and its handler with validation for project phase details 2025-12-27 19:42:13 +03:30
7ce7854091 add: include DeployStatus in ProjectDeployBoardListItem for deployment status tracking 2025-12-27 16:26:07 +03:30
10 changed files with 287 additions and 30 deletions

View File

@@ -0,0 +1,52 @@
using System.Linq;
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoUpdateDeployStatus;
public record AutoUpdateDeployStatusCommand : IBaseCommand;
public class AutoUpdateDeployStatusCommandHandler : IBaseCommandHandler<AutoUpdateDeployStatusCommand>
{
private readonly IProgramManagerDbContext _dbContext;
public AutoUpdateDeployStatusCommandHandler(IProgramManagerDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<OperationResult> Handle(AutoUpdateDeployStatusCommand request,
CancellationToken cancellationToken)
{
// Fetch all sections whose phase is still marked as not completed
var sections = await _dbContext.TaskSections
.Include(ts => ts.Task)
.ThenInclude(t => t.Phase)
.Where(ts => ts.Task.Phase.DeployStatus == ProjectDeployStatus.NotCompleted)
.ToListAsync(cancellationToken);
if (sections.Count == 0)
return OperationResult.Success();
var phasesToUpdate = sections
.GroupBy(ts => ts.Task.PhaseId)
.Where(g => g.All(s => s.Status == TaskSectionStatus.Completed))
.Select(g => g.First().Task.Phase)
.Distinct()
.ToList();
if (phasesToUpdate.Count == 0)
return OperationResult.Success();
foreach (var phase in phasesToUpdate)
{
phase.UpdateDeployStatus(ProjectDeployStatus.PendingDevDeploy);
}
await _dbContext.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
}

View File

@@ -0,0 +1,44 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeDeployStatusProject;
public record ChangeDeployStatusProjectCommand(Guid PhaseId, ProjectDeployStatus Status):IBaseCommand;
public class ChangeDeployStatusProjectCommandHandler : IBaseCommandHandler<ChangeDeployStatusProjectCommand>
{
private readonly IProjectRepository _projectRepository;
private readonly IProjectPhaseRepository _projectPhaseRepository;
private readonly IUnitOfWork _unitOfWork;
public ChangeDeployStatusProjectCommandHandler(IProjectRepository projectRepository, IUnitOfWork unitOfWork, IProjectPhaseRepository projectPhaseRepository)
{
_projectRepository = projectRepository;
_unitOfWork = unitOfWork;
_projectPhaseRepository = projectPhaseRepository;
}
public async Task<OperationResult> Handle(ChangeDeployStatusProjectCommand request, CancellationToken cancellationToken)
{
var project = await _projectPhaseRepository.GetByIdAsync(request.PhaseId, cancellationToken);
if (project == null)
return OperationResult.NotFound("بخش مورد نظر یافت نشد");
if (project.DeployStatus == ProjectDeployStatus.NotCompleted)
{
return OperationResult.Failure("وضعیت استقرار نمی‌تواند از حالت 'تایید نشده' تغییر کند.");
}
if (request.Status == ProjectDeployStatus.NotCompleted)
{
return OperationResult.Failure("وضعیت استقرار نمی‌تواند به حالت 'تایید نشده' تغییر کند.");
}
project.UpdateDeployStatus(request.Status);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
}

View File

@@ -0,0 +1,113 @@
using System.Security.AccessControl;
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardDetail;
public record ProjectDeployBoardDetailsResponse(
ProjectDeployBoardDetailPhaseItem Phase,
List<ProjectDeployBoardDetailTaskItem> Tasks);
public record ProjectDeployBoardDetailPhaseItem(
string Name,
TimeSpan TotalTimeSpan,
TimeSpan DoneTimeSpan);
public record ProjectDeployBoardDetailTaskItem(
string Name,
TimeSpan TotalTimeSpan,
TimeSpan DoneTimeSpan,
List<ProjectDeployBoardDetailItemSkill> Skills)
: ProjectDeployBoardDetailPhaseItem(Name, TotalTimeSpan, DoneTimeSpan);
public record ProjectDeployBoardDetailItemSkill(string OriginalUserFullName, string SkillName, int TimePercentage);
public record ProjectDeployBoardDetailsQuery(Guid PhaseId) : IBaseQuery<ProjectDeployBoardDetailsResponse>;
public class
ProjectDeployBoardDetailsQueryHandler : IBaseQueryHandler<ProjectDeployBoardDetailsQuery,
ProjectDeployBoardDetailsResponse>
{
private readonly IProgramManagerDbContext _dbContext;
public ProjectDeployBoardDetailsQueryHandler(IProgramManagerDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<OperationResult<ProjectDeployBoardDetailsResponse>> Handle(ProjectDeployBoardDetailsQuery request,
CancellationToken cancellationToken)
{
var phase = await _dbContext.ProjectPhases
.Include(x => x.Tasks)
.ThenInclude(x => x.Sections)
.ThenInclude(x => x.Activities)
.Include(x => x.Tasks)
.ThenInclude(x => x.Sections)
.ThenInclude(x => x.AdditionalTimes)
.Include(x => x.Tasks)
.ThenInclude(x => x.Sections)
.ThenInclude(x => x.Skill)
.FirstOrDefaultAsync(x => x.Id == request.PhaseId, cancellationToken);
if (phase == null)
return OperationResult<ProjectDeployBoardDetailsResponse>.NotFound("بخش اصلی مورد نظر یافت نشد");
var userIds = phase.Tasks
.SelectMany(t => t.Sections)
.Select(s => s.OriginalAssignedUserId)
.Distinct()
.ToList();
var usersDict = await _dbContext.Users
.Where(x => userIds.Contains(x.Id))
.ToDictionaryAsync(x => x.Id, x => x.FullName, cancellationToken);
var tasksRes = phase.Tasks.Select(t =>
{
var totalTime = t.Sections.Select(s => s.FinalEstimatedHours)
.Aggregate(TimeSpan.Zero, (sum, next) => sum.Add(next));
var doneTime = t.Sections.Aggregate(TimeSpan.Zero,
(sum, next) => sum.Add(next.GetTotalTimeSpent()));
var skills = t.Sections
.Select(s =>
{
var originalUserFullName = usersDict.GetValueOrDefault(s.OriginalAssignedUserId,
"کاربر ناشناس");
var skillName = s.Skill?.Name ?? "بدون مهارت";
var totalTimeSpent = s.GetTotalTimeSpent();
var timePercentage = s.FinalEstimatedHours.Ticks > 0
? (int)((totalTimeSpent.Ticks / (double)s.FinalEstimatedHours.Ticks) * 100)
: 0;
return new ProjectDeployBoardDetailItemSkill(
originalUserFullName,
skillName,
timePercentage);
}).ToList();
return new ProjectDeployBoardDetailTaskItem(
t.Name,
totalTime,
doneTime,
skills);
}).ToList();
var totalTimeSpan = tasksRes.Aggregate(TimeSpan.Zero,
(sum, next) => sum.Add(next.TotalTimeSpan));
var doneTimeSpan = tasksRes.Aggregate(TimeSpan.Zero,
(sum, next) => sum.Add(next.DoneTimeSpan));
var phaseRes = new ProjectDeployBoardDetailPhaseItem(phase.Name, totalTimeSpan, doneTimeSpan);
var res = new ProjectDeployBoardDetailsResponse(phaseRes, tasksRes);
return OperationResult<ProjectDeployBoardDetailsResponse>.Success(res);
}
}

View File

@@ -0,0 +1,11 @@
using FluentValidation;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardDetail;
public class ProjectDeployBoardDetailsQueryValidator:AbstractValidator<ProjectDeployBoardDetailsQuery>
{
public ProjectDeployBoardDetailsQueryValidator()
{
RuleFor(x=>x.PhaseId).NotNull().WithMessage("شناسه بخش اصلی نمی‌تواند خالی باشد");
}
}

View File

@@ -10,12 +10,14 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.Project
public record ProjectDeployBoardListItem()
{
public Guid Id { get; set; }
public string ProjectName { get; set; }
public string PhaseName { get; set; }
public int TotalTasks { get; set; }
public int DoneTasks { get; set; }
public TimeSpan TotalTimeSpan { get; set; }
public TimeSpan DoneTimeSpan { get; set; }
public ProjectDeployStatus DeployStatus { get; set; }
}
public record GetProjectsDeployBoardListResponse(List<ProjectDeployBoardListItem> Items);
@@ -43,23 +45,28 @@ public class ProjectDeployBoardListQueryHandler:IBaseQueryHandler<GetProjectDepl
}
var query =await _dbContext.TaskSections
.Include(x=>x.Activities)
.Include(x=>x.Task)
.ThenInclude(x => x.Phase)
.ThenInclude(x => x.Project)
.AsNoTracking()
.Where(x => x.Status == TaskSectionStatus.Completed
|| x.Status == TaskSectionStatus.PendingForCompletion
|| x.Task.Phase.DeployStatus != ProjectDeployStatus.NoTCompleted)
|| (x.Task.Phase.DeployStatus != ProjectDeployStatus.NotCompleted
&& x.InitialEstimatedHours>TimeSpan.Zero))
.GroupBy(x=>x.Task.PhaseId).ToListAsync(cancellationToken: cancellationToken);
var list = query.Select(g => new ProjectDeployBoardListItem
{
Id = g.Key,
ProjectName = g.First().Task.Phase.Project.Name,
PhaseName = g.First().Task.Phase.Name,
TotalTasks = g.Select(x => x.TaskId).Distinct().Count(),
DoneTasks = g.Where(x => x.Status == TaskSectionStatus.Completed).Select(x => x.TaskId).Distinct().Count(),
TotalTimeSpan = TimeSpan.FromHours(g.Sum(x => x.InitialEstimatedHours.TotalHours)),
DoneTimeSpan = TimeSpan.FromHours(g.Where(x => x.Status == TaskSectionStatus.Completed).Sum(x => x.InitialEstimatedHours.TotalHours))
DoneTasks = g.Where(x => x.Status == TaskSectionStatus.Completed)
.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
}).ToList();
var response = new GetProjectsDeployBoardListResponse(list);
return OperationResult<GetProjectsDeployBoardListResponse>.Success(response);

View File

@@ -23,7 +23,7 @@ public class ProjectPhase : ProjectHierarchyNode
ProjectId = projectId;
_tasks = new List<ProjectTask>();
_phaseSections = new List<PhaseSection>();
DeployStatus = ProjectDeployStatus.NoTCompleted;
DeployStatus = ProjectDeployStatus.NotCompleted;
AddDomainEvent(new PhaseCreatedEvent(Id, projectId, name));
}
@@ -211,12 +211,16 @@ public class ProjectPhase : ProjectHierarchyNode
public void UpdateDeployStatus(ProjectDeployStatus status)
{
DeployStatus = status;
if (status == ProjectDeployStatus.Deployed)
{
IsArchived = true;
}
}
}
public enum ProjectDeployStatus
{
NoTCompleted,
NotCompleted,
PendingDevDeploy,
DevDeployed,
PendingDeploy,

View File

@@ -5,7 +5,6 @@ namespace GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
public interface ITaskSectionActivityRepository:IRepository<Guid,TaskSectionActivity>
{
Task<TaskSectionActivity?> GetByIdAsync(Guid id);
Task<List<TaskSectionActivity>> GetBySectionIdAsync(Guid sectionId);
Task<List<TaskSectionActivity>> GetByUserIdAsync(long userId);
Task<List<TaskSectionActivity>> GetActiveByUserAsync(long userId);

View File

@@ -5,7 +5,7 @@ namespace GozareshgirProgramManager.Domain._Common;
public interface IRepository<TKey, T> where T:class
{
T Get(TKey id);
Task<T?> GetByIdAsync(TKey id);
Task<T?> GetByIdAsync(TKey id, CancellationToken cancellationToken = default);
List<T> Get();
IQueryable<T> GetQueryable();
void Create(T entity);

View File

@@ -43,9 +43,9 @@ public class RepositoryBase<TKey, T> : IRepository<TKey, T> where T : class
return _context.Find<T>(id);
}
public async Task<T?> GetByIdAsync(TKey id)
public async Task<T?> GetByIdAsync(TKey id, CancellationToken cancellationToken = default)
{
return await _context.Set<T>().FindAsync(id);
return await _context.Set<T>().FindAsync([id], cancellationToken);
}
public List<T> Get()

View File

@@ -2,6 +2,8 @@
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AssignProject;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoStopOverTimeTaskSections;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoUpdateDeployStatus;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeDeployStatusProject;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeStatusSection;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.CreateProject;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.DeleteProject;
@@ -12,6 +14,7 @@ using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectA
using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectBoardDetail;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectBoardList;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardDetail;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardList;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails;
using MediatR;
@@ -23,70 +26,75 @@ namespace ServiceHost.Areas.Admin.Controllers.ProgramManager;
public class ProjectController : ProgramManagerBaseController
{
private readonly IMediator _mediator;
public ProjectController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<ActionResult<OperationResult<GetProjectsListResponse>>> Get([FromQuery]GetProjectsListQuery query)
public async Task<ActionResult<OperationResult<GetProjectsListResponse>>> Get(
[FromQuery] GetProjectsListQuery query)
{
var res=await _mediator.Send(query);
var res = await _mediator.Send(query);
return res;
}
[HttpPost]
public async Task<ActionResult<OperationResult>> Create([FromBody] CreateProjectCommand command)
{
var res=await _mediator.Send(command);
var res = await _mediator.Send(command);
return res;
}
[HttpPut]
public async Task<ActionResult<OperationResult>> Edit([FromBody] EditProjectCommand command)
{
var res=await _mediator.Send(command);
var res = await _mediator.Send(command);
return res;
}
[HttpDelete]
public async Task<ActionResult<OperationResult>> Delete([FromQuery] DeleteProjectCommand command)
{
var res=await _mediator.Send(command);
var res = await _mediator.Send(command);
return res;
}
[HttpGet("assign")]
public async Task<ActionResult<OperationResult<GetProjectAssignDetailsResponse>>> GetAssignableProjects(GetProjectAssignDetailsQuery query)
public async Task<ActionResult<OperationResult<GetProjectAssignDetailsResponse>>> GetAssignableProjects(
GetProjectAssignDetailsQuery query)
{
var res=await _mediator.Send(query);
var res = await _mediator.Send(query);
return res;
}
[HttpPost("assign")]
public async Task<ActionResult<OperationResult>> Assign(AssignProjectCommand command)
{
var res=await _mediator.Send(command);
var res = await _mediator.Send(command);
return res;
}
[HttpGet("set-time")]
public async Task<ActionResult<OperationResult<ProjectSetTimeResponse>>> GetSetTimeProjectDetails(ProjectSetTimeDetailsQuery query)
public async Task<ActionResult<OperationResult<ProjectSetTimeResponse>>> GetSetTimeProjectDetails(
ProjectSetTimeDetailsQuery query)
{
var res=await _mediator.Send(query);
var res = await _mediator.Send(query);
return res;
}
[HttpPost("set-time")]
public async Task<ActionResult<OperationResult>> SetTimeProject(SetTimeProjectCommand command)
{
var res=await _mediator.Send(command);
var res = await _mediator.Send(command);
return res;
}
[HttpPost("change-status")]
public async Task<ActionResult<OperationResult>> ChangeStatus(ChangeStatusSectionCommand command)
{
var res = await _mediator.Send(command);
var res = await _mediator.Send(command);
return res;
}
@@ -98,14 +106,16 @@ public class ProjectController : ProgramManagerBaseController
}
[HttpGet("board")]
public async Task<ActionResult<OperationResult<List<ProjectBoardListResponse>>>> GetProjectBoard([FromQuery] ProjectBoardListQuery query)
public async Task<ActionResult<OperationResult<List<ProjectBoardListResponse>>>> GetProjectBoard(
[FromQuery] ProjectBoardListQuery query)
{
// اجرای Command برای متوقف کردن تسک‌های overtime قبل از نمایش
await _mediator.Send(new AutoStopOverTimeTaskSectionsCommand());
var res = await _mediator.Send(query);
return res;
}
[HttpGet("board/details")]
public async Task<ActionResult<OperationResult<ProjectBoardDetailResponse>>> GetProjectBoardDetails(Guid id)
{
@@ -117,7 +127,24 @@ public class ProjectController : ProgramManagerBaseController
[HttpGet("deploy-board")]
public async Task<ActionResult<OperationResult<GetProjectsDeployBoardListResponse>>> GetProjectDeployBoard()
{
// قبل از دریافت دیتا، وضعیت دیپلوی را بر اساس تکمیل شدن تمام سکشن‌ها به‌روزرسانی می‌کنیم
await _mediator.Send(new AutoUpdateDeployStatusCommand());
var request = new GetProjectDeployBoardListQuery();
return await _mediator.Send(request);
}
[HttpGet("deploy-board/details")]
public async Task<ActionResult<OperationResult<ProjectDeployBoardDetailsResponse>>> GetProjectDeployBoardDetails(Guid id)
{
var query = new ProjectDeployBoardDetailsQuery(id);
var res = await _mediator.Send(query);
return res;
}
[HttpPost("deploy-board/change-status")]
public async Task<ActionResult<OperationResult>> ChangeDeployStatus(ChangeDeployStatusProjectCommand command)
{
var res = await _mediator.Send(command);
return res;
}
}