From 6a446d597248d0c47eddad5a0a24a0d852b8cf3a Mon Sep 17 00:00:00 2001 From: mahan Date: Mon, 22 Dec 2025 19:13:34 +0330 Subject: [PATCH 1/2] add: implement AutoStopOverTimeTaskSections command and related functionality --- .../AutoStopOverTimeTaskSectionsCommand.cs | 49 +++++++++++++++++++ .../ProjectAgg/Entities/TaskSection.cs | 35 +++++++++++++ .../Entities/TaskSectionActivity.cs | 16 ++++++ .../Repositories/ITaskSectionRepository.cs | 2 + .../Repositories/TaskSectionRepository.cs | 10 ++++ .../ProgramManager/ProjectController.cs | 4 ++ 6 files changed, 116 insertions(+) create mode 100644 ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AutoStopOverTimeTaskSections/AutoStopOverTimeTaskSectionsCommand.cs diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AutoStopOverTimeTaskSections/AutoStopOverTimeTaskSectionsCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AutoStopOverTimeTaskSections/AutoStopOverTimeTaskSectionsCommand.cs new file mode 100644 index 00000000..892d4844 --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AutoStopOverTimeTaskSections/AutoStopOverTimeTaskSectionsCommand.cs @@ -0,0 +1,49 @@ +using GozareshgirProgramManager.Application._Common.Interfaces; +using GozareshgirProgramManager.Application._Common.Models; +using GozareshgirProgramManager.Domain._Common; +using GozareshgirProgramManager.Domain.ProjectAgg.Enums; +using GozareshgirProgramManager.Domain.ProjectAgg.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoStopOverTimeTaskSections; + +public record AutoStopOverTimeTaskSectionsCommand : IBaseCommand; + +public class AutoStopOverTimeTaskSectionsCommandHandler : IBaseCommandHandler +{ + private readonly ITaskSectionRepository _taskSectionRepository; + private readonly IUnitOfWork _unitOfWork; + + public AutoStopOverTimeTaskSectionsCommandHandler(ITaskSectionRepository taskSectionRepository, + IUnitOfWork unitOfWork) + { + _taskSectionRepository = taskSectionRepository; + _unitOfWork = unitOfWork; + } + + public async Task Handle(AutoStopOverTimeTaskSectionsCommand request, + CancellationToken cancellationToken) + { + try + { + // دریافت تمام تسک‌های در حال انجام + var taskSections = await _taskSectionRepository.GetActiveSectionsIncludeAllAsync(cancellationToken); + + + foreach (var taskSection in taskSections) + { + // استفاده از متد Domain برای بررسی و متوقف کردن خودکار + taskSection.AutoStopIfOverTime(); + } + + // ذخیره تغییرات در دیتابیس + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Success(); + } + catch (Exception ex) + { + return OperationResult.Failure($"خطا در ناتمام کردن تسک‌های overtime: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs index 7fd3f3e8..7209dd00 100644 --- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs @@ -217,4 +217,39 @@ public class TaskSection : EntityBase var finalEstimate = FinalEstimatedHours; return totalSpent < finalEstimate; } + + /// + /// اگر زمان کار شده بیش از تایم تعیین شده باشد، تسک را متوقف می‌کند + /// و EndDate را به طوری تنظیم می‌کند که کل زمان برابر با FinalEstimatedHours شود + /// + public void AutoStopIfOverTime() + { + if (Status != TaskSectionStatus.InProgress) + return; + + var activeActivity = _activities.FirstOrDefault(a => a.IsActive); + if (activeActivity == null) + return; + + // محاسبه کل زمان صرف شده تا کنون (بدون فعالیت فعال) + var totalTimeSpentExcludingActive = _activities.Where(a => !a.IsActive).Sum(a => a.GetTimeSpent().Ticks); + var totalTimeSpentTimeSpan = TimeSpan.FromTicks(totalTimeSpentExcludingActive); + var finalEstimate = FinalEstimatedHours; + + // اگر زمان صرف شده (بدون فعالیت فعال) + فعالیت فعال > تایم تعیین شده + var activeTimeSpent = activeActivity.GetTimeSpent(); + if (totalTimeSpentTimeSpan + activeTimeSpent > finalEstimate) + { + // محاسبه مدت زمانی که این فعالیت باید برای رسیدن به FinalEstimatedHours داشته باشد + var remainingTime = finalEstimate - totalTimeSpentTimeSpan; + + // EndDate = StartDate + remainingTime + var adjustedEndDate = activeActivity.StartDate.Add(remainingTime); + + // متوقف کردن فعالیت با EndDate دقیق شده + activeActivity.StopWorkWithSpecificTime(adjustedEndDate, "متوقف خودکار - بیش از تایم تعیین شده"); + + UpdateStatus(TaskSectionStatus.Incomplete); + } + } } \ No newline at end of file diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSectionActivity.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSectionActivity.cs index 85e60d0d..04b3a7e3 100644 --- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSectionActivity.cs +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSectionActivity.cs @@ -40,6 +40,22 @@ public class TaskSectionActivity : EntityBase IsActive = false; } + /// + /// متوقف کردن فعالیت با مشخص کردن EndDate دقیق + /// + public void StopWorkWithSpecificTime(DateTime endDate, string? endNotes = null) + { + if (!IsActive) + throw new InvalidOperationException("این فعالیت قبلاً متوقف شده است."); + + if (endDate < StartDate) + throw new InvalidOperationException("تاریخ پایان نمی‌تواند قبل از تاریخ شروع باشد."); + + EndDate = endDate; + EndNotes = endNotes; + IsActive = false; + } + public TimeSpan GetTimeSpent() { if (IsActive) diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/ITaskSectionRepository.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/ITaskSectionRepository.cs index d9c11d75..b4a4c5dd 100644 --- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/ITaskSectionRepository.cs +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/ITaskSectionRepository.cs @@ -1,3 +1,4 @@ +using System.Collections; using GozareshgirProgramManager.Domain._Common; using GozareshgirProgramManager.Domain.ProjectAgg.Entities; @@ -11,4 +12,5 @@ public interface ITaskSectionRepository: IRepository Task GetByIdWithFullDataAsync(Guid id, CancellationToken cancellationToken = default); Task> GetAssignedToUserAsync(long userId); + Task> GetActiveSectionsIncludeAllAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs index 41551be5..89fb05fd 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs @@ -1,4 +1,5 @@ using GozareshgirProgramManager.Domain.ProjectAgg.Entities; +using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Repositories; using GozareshgirProgramManager.Infrastructure.Persistence._Common; using GozareshgirProgramManager.Infrastructure.Persistence.Context; @@ -35,4 +36,13 @@ public class TaskSectionRepository:RepositoryBase,ITaskSection .Where(x => x.CurrentAssignedUserId == userId) .ToListAsync(); } + + public Task> GetActiveSectionsIncludeAllAsync(CancellationToken cancellationToken) + { + return _context.TaskSections + .Where(x => x.Status == TaskSectionStatus.InProgress) + .Include(x => x.Activities) + .Include(x => x.AdditionalTimes) + .ToListAsync(cancellationToken); + } } \ No newline at end of file diff --git a/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs b/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs index dc46d47a..225a1e9b 100644 --- a/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs +++ b/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs @@ -1,6 +1,7 @@ using System.Runtime.InteropServices; 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.ChangeStatusSection; using GozareshgirProgramManager.Application.Modules.Projects.Commands.CreateProject; using GozareshgirProgramManager.Application.Modules.Projects.Commands.DeleteProject; @@ -98,6 +99,9 @@ public class ProjectController : ProgramManagerBaseController [HttpGet("board")] public async Task>>> GetProjectBoard([FromQuery] ProjectBoardListQuery query) { + // اجرای Command برای متوقف کردن تسک‌های overtime قبل از نمایش + await _mediator.Send(new AutoStopOverTimeTaskSectionsCommand()); + var res = await _mediator.Send(query); return res; } From 1bfe41418ba4cdb99707feb8bba85cdf806b0a4d Mon Sep 17 00:00:00 2001 From: mahan Date: Mon, 22 Dec 2025 19:51:21 +0330 Subject: [PATCH 2/2] add: enhance ProjectBoardDetailResponse to include RemainingTime and user time details --- .../ProjectBoardDetailQueryHandler.cs | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardDetail/ProjectBoardDetailQueryHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardDetail/ProjectBoardDetailQueryHandler.cs index 467ed43e..60de81c1 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardDetail/ProjectBoardDetailQueryHandler.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardDetail/ProjectBoardDetailQueryHandler.cs @@ -1,3 +1,4 @@ +using System.Globalization; using GozareshgirProgramManager.Application._Common.Interfaces; using GozareshgirProgramManager.Application._Common.Models; using GozareshgirProgramManager.Domain._Common; @@ -7,21 +8,23 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.Project public record ProjectBoardDetailQuery(Guid SectionId) : IBaseQuery; -public record ProjectBoardDetailResponse(List Users, string TotalTime); +public record ProjectBoardDetailResponse(List Users, string TotalTime,string RemainingTime ); public record ProjectBoardDetailUserResponse { - public List Histories { get; set; } = new(); - public string UserFullName { get; set; } - public long UserId { get; set; } + public List Histories { get; init; } + public string UserFullName { get; init; } + public string TotalTime { get; init; } + public string SpentTime { get; init; } + public long UserId { get; init; } } -public class ProjectBoardDetailUserHistoryResponse +public record ProjectBoardDetailUserHistoryResponse { - public string Date { get; set; } - public string startTime { get; set; } - public string EndTime { get; set; } - public string TotalTime { get; set; } + public string Date { get; init; } + public string startTime { get; init; } + public string EndTime { get; init; } + public string TotalTime { get; init; } } public class ProjectBoardDetailQueryHandler : IBaseQueryHandler @@ -38,6 +41,7 @@ public class ProjectBoardDetailQueryHandler : IBaseQueryHandler x.Activities) + .Include(x=>x.AdditionalTimes) .FirstOrDefaultAsync(x => x.Id == request.SectionId, cancellationToken: cancellationToken); if (section == null) @@ -49,16 +53,22 @@ public class ProjectBoardDetailQueryHandler : IBaseQueryHandler userIds.Contains(x.Id)) .ToDictionaryAsync(x => x.Id, x => x.FullName, cancellationToken); - var totalTimeSpan = section.Activities + var totalActivityTimeSpan = section.Activities .Select(x => x.GetTimeSpent()) .Aggregate(TimeSpan.Zero, (sum, next) => sum.Add(next)); + var finalTime = section.FinalEstimatedHours; + + var remainingTimeSpan = finalTime >= totalActivityTimeSpan + ? TimeSpan.FromTicks(finalTime.Ticks - totalActivityTimeSpan.Ticks) + : TimeSpan.Zero; var users = section.Activities.GroupBy(x => x.UserId).Select(x => { return new ProjectBoardDetailUserResponse() { UserId = x.Key, UserFullName = usersDict[x.Key], + TotalTime = TimeSpan.FromTicks(x.Sum(h=>h.GetTimeSpent().Ticks)).TotalHours.ToString(CultureInfo.InvariantCulture), Histories = x.Select(h => new ProjectBoardDetailUserHistoryResponse() { Date = h.StartDate.ToFarsi(), @@ -68,7 +78,8 @@ public class ProjectBoardDetailQueryHandler : IBaseQueryHandler.Success(response); } } \ No newline at end of file