467 lines
20 KiB
C#
467 lines
20 KiB
C#
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||
using GozareshgirProgramManager.Application._Common.Models;
|
||
using GozareshgirProgramManager.Application.Modules.Projects.DTOs;
|
||
using GozareshgirProgramManager.Domain._Common;
|
||
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;
|
||
|
||
public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCommand>
|
||
{
|
||
private readonly IProjectRepository _projectRepository;
|
||
private readonly IProjectPhaseRepository _projectPhaseRepository;
|
||
private readonly IProjectTaskRepository _projectTaskRepository;
|
||
private readonly IUnitOfWork _unitOfWork;
|
||
private readonly IUserRepository _userRepository;
|
||
private readonly ISkillRepository _skillRepository;
|
||
private readonly IPhaseSectionRepository _phaseSectionRepository;
|
||
private readonly IProjectSectionRepository _projectSectionRepository;
|
||
private long? _userId;
|
||
private readonly ITaskSectionRepository _taskSectionRepository;
|
||
|
||
|
||
public SetTimeProjectCommandHandler(
|
||
IProjectRepository projectRepository,
|
||
IProjectPhaseRepository projectPhaseRepository,
|
||
IProjectTaskRepository projectTaskRepository,
|
||
IUnitOfWork unitOfWork, IAuthHelper authHelper,
|
||
IUserRepository userRepository, ISkillRepository skillRepository,
|
||
IPhaseSectionRepository phaseSectionRepository,
|
||
IProjectSectionRepository projectSectionRepository,
|
||
ITaskSectionRepository taskSectionRepository)
|
||
{
|
||
_projectRepository = projectRepository;
|
||
_projectPhaseRepository = projectPhaseRepository;
|
||
_projectTaskRepository = projectTaskRepository;
|
||
_unitOfWork = unitOfWork;
|
||
_userRepository = userRepository;
|
||
_skillRepository = skillRepository;
|
||
_phaseSectionRepository = phaseSectionRepository;
|
||
_projectSectionRepository = projectSectionRepository;
|
||
_taskSectionRepository = taskSectionRepository;
|
||
_userId = authHelper.GetCurrentUserId();
|
||
}
|
||
|
||
public async Task<OperationResult> Handle(SetTimeProjectCommand request, CancellationToken cancellationToken)
|
||
{
|
||
switch (request.Level)
|
||
{
|
||
case ProjectHierarchyLevel.Project:
|
||
return await AssignProject(request);
|
||
case ProjectHierarchyLevel.Phase:
|
||
return await AssignProjectPhase(request);
|
||
case ProjectHierarchyLevel.Task:
|
||
return await SetTimeForProjectTask(request, cancellationToken);
|
||
default:
|
||
return OperationResult.Failure("سطح پروژه نامعتبر است");
|
||
}
|
||
}
|
||
|
||
private async Task<OperationResult> AssignProject(SetTimeProjectCommand request)
|
||
{
|
||
var project = await _projectRepository.GetWithFullHierarchyAsync(request.Id);
|
||
if (project is null)
|
||
{
|
||
return OperationResult.NotFound("پروژه یافت نشد");
|
||
}
|
||
|
||
var skillItems = request.SkillItems.Where(x=>x.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)
|
||
{
|
||
var skill = await _skillRepository.GetByIdAsync(item.SkillId);
|
||
if (skill is null)
|
||
{
|
||
return OperationResult.NotFound($"مهارت با شناسه {item.SkillId} یافت نشد");
|
||
}
|
||
|
||
// بررسی و بهروزرسانی یا اضافه کردن 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)
|
||
{
|
||
// اگر CascadeToChildren true است یا فاز override ندارد
|
||
if (request.CascadeToChildren || !phase.HasAssignmentOverride)
|
||
{
|
||
// حذف PhaseSections که در validSkills نیستند
|
||
var phaseSectionsToRemove = phase.PhaseSections
|
||
.Where(s => !validSkillIds.Contains(s.SkillId))
|
||
.ToList();
|
||
|
||
foreach (var section in phaseSectionsToRemove)
|
||
{
|
||
phase.RemovePhaseSection(section.SkillId);
|
||
}
|
||
|
||
// برای phase هم باید sectionها را بهروزرسانی کنیم
|
||
foreach (var item in skillItems )
|
||
{
|
||
var existingSection = phase.PhaseSections.FirstOrDefault(s => s.SkillId == item.SkillId);
|
||
if (existingSection != null)
|
||
{
|
||
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)
|
||
{
|
||
// حذف TaskSections که در validSkills نیستند
|
||
var taskSectionsToRemove = task.Sections
|
||
.Where(s => !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);
|
||
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();
|
||
return OperationResult.Success();
|
||
}
|
||
|
||
private async Task<OperationResult> AssignProjectPhase(SetTimeProjectCommand request)
|
||
{
|
||
var phase = await _projectPhaseRepository.GetWithTasksAsync(request.Id);
|
||
if (phase is null)
|
||
{
|
||
return OperationResult.NotFound("فاز پروژه یافت نشد");
|
||
}
|
||
|
||
// تخصیص در سطح فاز
|
||
foreach (var item in request.SkillItems)
|
||
{
|
||
var skill = await _skillRepository.GetByIdAsync(item.SkillId);
|
||
if (skill is null)
|
||
{
|
||
return OperationResult.NotFound($"مهارت با شناسه {item.SkillId} یافت نشد");
|
||
}
|
||
}
|
||
|
||
// علامتگذاری که این فاز نسبت به parent متمایز است
|
||
phase.MarkAsOverridden();
|
||
|
||
var skillItems = request.SkillItems.Where(x=>x.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)
|
||
{
|
||
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)
|
||
{
|
||
// اگر CascadeToChildren true است یا تسک override ندارد
|
||
if (request.CascadeToChildren || !task.HasAssignmentOverride)
|
||
{
|
||
// حذف TaskSections که در validSkills نیستند
|
||
var taskSectionsToRemove = task.Sections
|
||
.Where(s => !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);
|
||
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();
|
||
return OperationResult.Success();
|
||
}
|
||
|
||
|
||
private async Task<OperationResult> SetTimeForProjectTask(SetTimeProjectCommand request,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
var task = await _projectTaskRepository.GetWithSectionsAsync(request.Id);
|
||
if (task == null)
|
||
{
|
||
return OperationResult.NotFound("تسک یافت نشد");
|
||
}
|
||
|
||
long? addedByUserId = _userId;
|
||
|
||
var validSkills = request.SkillItems
|
||
.Where(x=>x.UserId is > 0).ToList();
|
||
|
||
// حذف سکشنهایی که در 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)
|
||
{
|
||
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 ValidateTotalTimeNotLessThanSpent(TimeSpan newTotalTime, TimeSpan currentTotalSpent)
|
||
{
|
||
if (newTotalTime < currentTotalSpent)
|
||
{
|
||
throw new BadRequestException(
|
||
$"تایم کل سکشن نمیتواند کمتر از زمان مصرف شده ({currentTotalSpent.TotalHours:F2} ساعت) باشد");
|
||
}
|
||
}
|
||
|
||
private void SetSectionTime(TaskSection section, SetTimeProjectSkillItem sectionItem, long? addedByUserId)
|
||
{
|
||
var initData = sectionItem.InitData;
|
||
var initialTime = TimeSpan.FromHours(initData.Hours)
|
||
.Add(TimeSpan.FromMinutes(initData.Minutes));
|
||
|
||
if (initialTime <= TimeSpan.Zero)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// تنظیم زمان اولیه
|
||
section.UpdateInitialEstimatedHours(initialTime, initData.Description);
|
||
|
||
// مدیریت هوشمند زمانهای اضافی
|
||
var existingAdditionalTimes = section.AdditionalTimes.ToList();
|
||
var incomingAdditionalTimes = sectionItem.AdditionalTime ?? [];
|
||
var currentTotalSpent = section.GetTotalTimeSpent();
|
||
|
||
bool hasRealChange = false;
|
||
|
||
// حذف آیتمهایی که دیگر در لیست نیستند
|
||
foreach (var existingTime in existingAdditionalTimes)
|
||
{
|
||
var stillExists = incomingAdditionalTimes.Any(x => x.Id == existingTime.Id);
|
||
if (!stillExists)
|
||
{
|
||
section.RemoveAdditionalTime(existingTime.Id);
|
||
hasRealChange = true;
|
||
}
|
||
}
|
||
|
||
// ویرایش یا اضافه کردن آیتمهای جدید
|
||
foreach (var additionalTime in incomingAdditionalTimes)
|
||
{
|
||
var additionalTimeSpan = TimeSpan.FromHours(additionalTime.Hours)
|
||
.Add(TimeSpan.FromMinutes(additionalTime.Minutes));
|
||
|
||
if (additionalTimeSpan <= TimeSpan.Zero)
|
||
continue;
|
||
|
||
var existingAdditionalTime = existingAdditionalTimes.FirstOrDefault(x => x.Id == additionalTime.Id);
|
||
|
||
if (existingAdditionalTime != null)
|
||
{
|
||
// اگر آیتم با این ID وجود دارد، بررسی کن اگر تغییر کرده باشد
|
||
if (existingAdditionalTime.HasChanged(additionalTimeSpan, additionalTime.Description))
|
||
{
|
||
var newTotalTime = section.InitialEstimatedHours
|
||
.Add(existingAdditionalTimes
|
||
.Where(x => x.Id != existingAdditionalTime.Id)
|
||
.Aggregate(TimeSpan.Zero, (acc, x) => acc.Add(x.Hours))
|
||
.Add(additionalTimeSpan));
|
||
|
||
ValidateTotalTimeNotLessThanSpent(newTotalTime, currentTotalSpent);
|
||
|
||
// ویرایش بدون حذف و ایجاد دوباره
|
||
existingAdditionalTime.Update(additionalTimeSpan, additionalTime.Description);
|
||
hasRealChange = true;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// اگر ID نداشت یا ID جدید بود، اضافه کن
|
||
if (additionalTime.Id == null || additionalTime.Id == Guid.Empty)
|
||
{
|
||
var newTotalTime = section.FinalEstimatedHours.Add(additionalTimeSpan);
|
||
ValidateTotalTimeNotLessThanSpent(newTotalTime, currentTotalSpent);
|
||
|
||
section.AddAdditionalTime(additionalTimeSpan, additionalTime.Description, addedByUserId);
|
||
hasRealChange = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// تغییر status به Incomplete فقط اگر تغییری واقعی اعمال شده باشد و در وضعیتی غیر از ReadyToStart باشد
|
||
if (hasRealChange && section.Status != TaskSectionStatus.ReadyToStart)
|
||
{
|
||
// اگر سکشن درحال انجام است، باید متوقف شود قبل از تغییر status
|
||
if (section.Status == TaskSectionStatus.InProgress)
|
||
{
|
||
section.StopWork(TaskSectionStatus.Incomplete);
|
||
}
|
||
else
|
||
{
|
||
section.UpdateStatus(TaskSectionStatus.Incomplete);
|
||
}
|
||
}
|
||
}
|
||
|
||
// private void SetSectionTime(ProjectSection section, SetTimeProjectSectionItem sectionItem, long? addedByUserId)
|
||
// {
|
||
// var initData = sectionItem.InitData;
|
||
// var initialTime = TimeSpan.FromHours(initData.Hours);
|
||
//
|
||
// // تنظیم زمان اولیه
|
||
// section.UpdateInitialEstimatedHours(initialTime, initData.Description);
|
||
//
|
||
// section.ClearAdditionalTimes();
|
||
// // افزودن زمانهای اضافی
|
||
// foreach (var additionalTime in sectionItem.AdditionalTime)
|
||
// {
|
||
// var additionalTimeSpan = TimeSpan.FromHours(additionalTime.Hours);
|
||
// section.AddAdditionalTime(additionalTimeSpan, additionalTime.Description, addedByUserId);
|
||
// }
|
||
// }
|
||
} |