Files
Backend-Api/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs
mahan 8d93fa4fc6 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
2026-01-04 20:39:20 +03:30

107 lines
5.4 KiB
C#

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.ChangeStatusSection;
public record ChangeStatusSectionCommand(Guid SectionId, TaskSectionStatus Status) : IBaseCommand;
public class ChangeStatusSectionCommandHandler : IBaseCommandHandler<ChangeStatusSectionCommand>
{
private readonly IUnitOfWork _unitOfWork;
private readonly ITaskSectionRepository _taskSectionRepository;
private readonly IAuthHelper _authHelper;
public ChangeStatusSectionCommandHandler(ITaskSectionRepository taskSectionRepository,
IUnitOfWork unitOfWork, IAuthHelper authHelper)
{
_taskSectionRepository = taskSectionRepository;
_unitOfWork = unitOfWork;
_authHelper = authHelper;
}
public async Task<OperationResult> Handle(ChangeStatusSectionCommand request, CancellationToken cancellationToken)
{
// استفاده از متد مخصوص که Activities رو load می‌کنه
var section = await _taskSectionRepository.GetByIdWithActivitiesAsync(request.SectionId, cancellationToken);
if (section == null)
return OperationResult.NotFound("بخش مورد نظر یافت نشد");
if (section.Status == request.Status)
return OperationResult.Success();
long currentUser = _authHelper.GetCurrentUserId()
?? throw new UnAuthorizedException("کاربر احراز هویت نشده است");
// Validate state transitions
var validationResult = ValidateStateTransition(section.Status, request.Status);
if (!validationResult.IsSuccess)
return validationResult;
// Handle state machine logic
if (section.Status == TaskSectionStatus.InProgress)
{
// Coming FROM InProgress: Stop the active activity
section.StopWork(request.Status);
}
else if (request.Status == TaskSectionStatus.InProgress)
{
// Going TO InProgress: Check if section has remaining time, then start work
if (!section.HasRemainingTime())
return OperationResult.ValidationError("زمان این بخش به پایان رسیده است");
if (await _taskSectionRepository.HasUserAnyInProgressSectionAsync(section.CurrentAssignedUserId, cancellationToken))
{
return OperationResult.ValidationError("کاربر مورد نظر در حال حاضر بخش دیگری را در وضعیت 'درحال انجام' دارد");
}
section.StartWork();
}
else
{
// All other transitions: Just update status
section.UpdateStatus(request.Status);
}
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
/// <summary>
/// Validates state transitions based on business rules:
/// - ReadyToStart: شروع نشده - Initial state only, cannot return to it once left
/// - InProgress: درحال انجام - Can transition from ReadyToStart, can go to Incomplete or Completed
/// - Incomplete: نیمه کاره - Can come from InProgress or other states
/// - Completed: اتمام رسیده - Can come from InProgress or other states
/// </summary>
private OperationResult ValidateStateTransition(TaskSectionStatus currentStatus, TaskSectionStatus targetStatus)
{
// Cannot transition to ReadyToStart once the section has been started
if (targetStatus == TaskSectionStatus.ReadyToStart)
return OperationResult.ValidationError("بخش نمی‌تواند به وضعیت 'آماده برای شروع' تغییر کند");
// From ReadyToStart, can only go to InProgress
if (currentStatus == TaskSectionStatus.ReadyToStart && targetStatus != TaskSectionStatus.InProgress)
return OperationResult.ValidationError("از وضعیت 'آماده برای شروع' فقط می‌توان به 'درحال انجام' رفت");
// Valid transitions matrix
var validTransitions = new Dictionary<TaskSectionStatus, List<TaskSectionStatus>>
{
{ TaskSectionStatus.ReadyToStart, [TaskSectionStatus.InProgress] },
{ 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] }
};
if (!validTransitions.TryGetValue(currentStatus, out var allowedTargets))
return OperationResult.ValidationError($"وضعیت فعلی '{currentStatus}' نامعتبر است");
if (!allowedTargets.Contains(targetStatus))
return OperationResult.ValidationError(
$"نمی‌توان از وضعیت '{currentStatus}' به '{targetStatus}' رفت");
return OperationResult.Success();
}
}