Merge branch 'Feature/program-manager/request-additional-time' into Main

This commit is contained in:
2026-01-26 18:32:16 +03:30
21 changed files with 393 additions and 70 deletions

View File

@@ -25,9 +25,4 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Folder Include="Modules\TaskSectionRevision\Commands\" />
<Folder Include="Modules\TaskSectionTimeRequests\Queries\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionRevision.Commands.CreateTaskSectionRevision;
public class CreateTaskSectionRevisionValidator:AbstractValidator<CreateTaskSectionRevisionCommand>
{
public CreateTaskSectionRevisionValidator()
{
RuleFor(x=>x.Message)
.NotEmpty()
.WithMessage("توضیحات اجباری است");
}
}

View File

@@ -0,0 +1,35 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionRevision.Commands.SetStatusReviewedRevision;
public record SetStatusReviewedRevisionCommand(Guid TaskSectionId):IBaseCommand;
public class SetStatusReviewedRevisionCommandHandler : IBaseCommandHandler<SetStatusReviewedRevisionCommand>
{
private readonly ITaskSectionRevisionRepository _revisionRepository;
private readonly IUnitOfWork _unitOfWork;
public SetStatusReviewedRevisionCommandHandler(ITaskSectionRevisionRepository revisionRepository, IUnitOfWork unitOfWork)
{
_revisionRepository = revisionRepository;
_unitOfWork = unitOfWork;
}
public async Task<OperationResult> Handle(SetStatusReviewedRevisionCommand request, CancellationToken cancellationToken)
{
var taskSectionRevisions = await _revisionRepository.GetByTaskSectionId(request.TaskSectionId);
if (taskSectionRevisions == null || taskSectionRevisions.Count == 0)
return OperationResult.NotFound("اصلاحی برای این بخش یافت نشد");
foreach (var revision in taskSectionRevisions)
{
revision.MarkReviewed();
}
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
}

View File

@@ -1,3 +1,5 @@
using GozareshgirProgramManager.Application._Common.Models;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionRevision.Queries.TaskRevisionsByTaskSectionId;
public record TaskRevisionsByTaskSectionIdResponse(
@@ -11,34 +13,4 @@ public record TaskRevisionsByTaskSectionIdResponse(
public record TaskRevisionsByTaskSectionIdItem(string Message, List<TaskRevisionsByTaskSectionIdItemFile> Files,string CreationDate);
public class TaskRevisionsByTaskSectionIdItemFile
{
public Guid Id { get; set; }
public string FileName { get; set; } = string.Empty;
public string FileUrl { get; set; } = string.Empty;
public long FileSizeBytes { get; set; }
public string FileType { get; set; } = string.Empty;
public string? ThumbnailUrl { get; set; }
public int? ImageWidth { get; set; }
public int? ImageHeight { get; set; }
public int? DurationSeconds { get; set; }
public string FileSizeFormatted
{
get
{
const long kb = 1024;
const long mb = kb * 1024;
const long gb = mb * 1024;
if (FileSizeBytes >= gb)
return $"{FileSizeBytes / (double)gb:F2} GB";
if (FileSizeBytes >= mb)
return $"{FileSizeBytes / (double)mb:F2} MB";
if (FileSizeBytes >= kb)
return $"{FileSizeBytes / (double)kb:F2} KB";
return $"{FileSizeBytes} Bytes";
}
}
}
public class TaskRevisionsByTaskSectionIdItemFile:UploadedFileDto;

View File

@@ -1,7 +1,8 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionTimeRequests.Commands.AcceptTimeRequest;
@@ -13,10 +14,12 @@ public class AcceptTimeRequestCommandHandler:IBaseCommandHandler<AcceptTimeReque
{
private readonly ITaskSectionTimeRequestRepository _timeRequestRepository;
private readonly ITaskSectionRepository _taskSectionRepository;
public AcceptTimeRequestCommandHandler(ITaskSectionTimeRequestRepository timeRequestRepository, ITaskSectionRepository taskSectionRepository)
private readonly IUnitOfWork _unitOfWork;
public AcceptTimeRequestCommandHandler(ITaskSectionTimeRequestRepository timeRequestRepository, ITaskSectionRepository taskSectionRepository, IUnitOfWork unitOfWork)
{
_timeRequestRepository = timeRequestRepository;
_taskSectionRepository = taskSectionRepository;
_unitOfWork = unitOfWork;
}
public async Task<OperationResult> Handle(AcceptTimeRequestCommand request, CancellationToken cancellationToken)
@@ -33,11 +36,21 @@ public class AcceptTimeRequestCommandHandler:IBaseCommandHandler<AcceptTimeReque
{
return OperationResult.NotFound("بخش فرعی وارد شده نامعتبر است");
}
if (timeRequest.RequestStatus == TaskSectionTimeRequestStatus.Accepted)
{
return OperationResult.Failure("این درخواست قبلا تایید شده است");
}
// تایید درخواست زمان
timeRequest.AcceptTimeRequest();
// اضافه کردن زمان به TaskSection
var totalMinutes = (request.Hour * 60) + request.Minute;
var additionalTime = TimeSpan.FromMinutes(totalMinutes);
taskSection.AddAdditionalTime(additionalTime, request.TimeType, timeRequest.Description);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
// if (timeRequest.RequestType.)
// {
//
// }
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,49 @@
using GozareshgirProgramManager.Application._Common.Extensions;
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionTimeRequests.Queries.CreateTimeRequestDetails;
public record CreateTimeRequestDetailsResponse(List<CreateTimeRequestDetailsRevision> Revisions);
public record CreateTimeRequestDetailsRevision(string Message, List<UploadedFileDto> Files);
public record CreateTimeRequestDetailsQuery(Guid TaskSectionId) : IBaseQuery<CreateTimeRequestDetailsResponse>;
public class
CreateTimeRequestDetailsQueryHandler : IBaseQueryHandler<CreateTimeRequestDetailsQuery,
CreateTimeRequestDetailsResponse>
{
private readonly IProgramManagerDbContext _context;
public CreateTimeRequestDetailsQueryHandler(IProgramManagerDbContext context)
{
_context = context;
}
public async Task<OperationResult<CreateTimeRequestDetailsResponse>> Handle(CreateTimeRequestDetailsQuery request,
CancellationToken cancellationToken)
{
var revisions = await _context.TaskSectionRevisions.Where(x =>
x.TaskSectionId == request.TaskSectionId && x.Status == RevisionReviewStatus.Pending).ToListAsync(cancellationToken: cancellationToken);
var fileIds = revisions.SelectMany(x => x.Files)
.Select(x => x.Id).ToList();
var files =await _context.UploadedFiles
.Where(x => fileIds.Contains(x.Id)).ToListAsync(cancellationToken: cancellationToken);
var resItem = revisions.Select(x =>
{
var selectFileIds = x.Files.Select(f => f.FileId).ToList();
var filesDto = files.Where(f => selectFileIds.Contains(f.Id))
.Select(f => f.ToDto()).ToList();
return new CreateTimeRequestDetailsRevision(x.Message, filesDto);
}).ToList();
var res = new CreateTimeRequestDetailsResponse(resItem);
return OperationResult<CreateTimeRequestDetailsResponse>.Success(res);
}
}

View File

@@ -0,0 +1,11 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList;
namespace GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList;
public interface IWorkflowProvider
{
WorkflowType Type { get; }
Task<List<WorkflowListItem>> GetItems(long currentUserId, IProgramManagerDbContext context, CancellationToken cancellationToken);
Task<int> GetCount(long currentUserId, IProgramManagerDbContext context, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,31 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList.Providers;
public class NotAssignedWorkflowProvider : IWorkflowProvider
{
public WorkflowType Type => WorkflowType.NotAssigned;
public async Task<List<WorkflowListItem>> GetItems(long currentUserId, IProgramManagerDbContext context, CancellationToken cancellationToken)
{
// Assuming 0 means unassigned in CurrentAssignedUserId
var sections = await context.TaskSections
.Where(x => x.CurrentAssignedUserId == 0)
.ToListAsync(cancellationToken);
return sections.Select(ts => new WorkflowListItem
{
EntityId = ts.Id,
Title = "تخصیص‌ نیافته",
Type = WorkflowType.NotAssigned
}).ToList();
}
public async Task<int> GetCount(long currentUserId, IProgramManagerDbContext context, CancellationToken cancellationToken)
{
return await context.TaskSections
.Where(x => x.CurrentAssignedUserId == 0)
.CountAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,41 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList.Providers;
public class RejectedRevisionsWorkflowProvider : IWorkflowProvider
{
public WorkflowType Type => WorkflowType.Rejected;
public async Task<List<WorkflowListItem>> GetItems(long currentUserId, IProgramManagerDbContext context, CancellationToken cancellationToken)
{
var query = from revision in context.TaskSectionRevisions
.Where(x => x.Status == RevisionReviewStatus.Pending)
join taskSection in context.TaskSections
on revision.TaskSectionId equals taskSection.Id
where taskSection.CurrentAssignedUserId == currentUserId
select taskSection;
var sections = await query.ToListAsync(cancellationToken);
return sections.Select(ts => new WorkflowListItem
{
EntityId = ts.Id,
Title = "برگشت از سمت مدیر",
Type = WorkflowType.Rejected
}).ToList();
}
public async Task<int> GetCount(long currentUserId, IProgramManagerDbContext context, CancellationToken cancellationToken)
{
var query = from revision in context.TaskSectionRevisions
.Where(x => x.Status == RevisionReviewStatus.Pending)
join taskSection in context.TaskSections
on revision.TaskSectionId equals taskSection.Id
where taskSection.CurrentAssignedUserId == currentUserId
select revision.Id;
return await query.CountAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,50 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList.Providers;
namespace GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList;
public record WorkflowCountResponse(int Total, int Rejected, int NotAssigned, int PendingForApproval);
public record WorkflowCountQuery() : IBaseQuery<WorkflowCountResponse>;
public class WorkflowCountQueryHandler : IBaseQueryHandler<WorkflowCountQuery, WorkflowCountResponse>
{
private readonly IProgramManagerDbContext _context;
private readonly IAuthHelper _authHelper;
private readonly IEnumerable<IWorkflowProvider> _providers;
public WorkflowCountQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper, IEnumerable<IWorkflowProvider> providers)
{
_context = context;
_authHelper = authHelper;
_providers = providers;
}
public async Task<OperationResult<WorkflowCountResponse>> Handle(WorkflowCountQuery request, CancellationToken cancellationToken)
{
long currentUserId = _authHelper.GetCurrentUserId()!.Value;
int rejectedCount = 0;
int notAssignedCount = 0;
int pendingForApprovalCount = 0;
foreach (var provider in _providers)
{
var count = await provider.GetCount(currentUserId, _context, cancellationToken);
switch (provider.Type)
{
case WorkflowType.Rejected:
rejectedCount += count; break;
case WorkflowType.NotAssigned:
notAssignedCount += count; break;
case WorkflowType.PendingForApproval:
pendingForApprovalCount += count; break;
}
}
var total = rejectedCount + notAssignedCount + pendingForApprovalCount;
var response = new WorkflowCountResponse(total, rejectedCount, notAssignedCount, pendingForApprovalCount);
return OperationResult<WorkflowCountResponse>.Success(response);
}
}

View File

@@ -1,7 +1,7 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using Microsoft.EntityFrameworkCore;
using GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList.Providers;
namespace GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList;
@@ -27,36 +27,30 @@ public class WorkflowListQueryHandler:IBaseQueryHandler<WorkflowListQuery,Workfl
{
private readonly IProgramManagerDbContext _context;
private readonly IAuthHelper _authHelper;
private readonly IEnumerable<IWorkflowProvider> _providers;
public WorkflowListQueryHandler(IProgramManagerDbContext context,
IAuthHelper authHelper)
IAuthHelper authHelper,
IEnumerable<IWorkflowProvider> providers)
{
_context = context;
_authHelper = authHelper;
_providers = providers;
}
public async Task<OperationResult<WorkflowListResponse>> Handle(WorkflowListQuery request, CancellationToken cancellationToken)
{
var currentUserId =_authHelper.GetCurrentUserId();
var currentUserId = _authHelper.GetCurrentUserId()!.Value;
var items = new List<WorkflowListItem>();
var query = await (from revision in _context.TaskSectionRevisions
.Where(x=>x.Status == RevisionReviewStatus.Pending)
join taskSection in _context.TaskSections.Where(x => x.CurrentAssignedUserId == currentUserId)
on revision.TaskSectionId equals taskSection.Id
select new
{
taskSection,
revision
}).ToListAsync(cancellationToken: cancellationToken);
var workflowListItems = query.Select(x=>new WorkflowListItem()
foreach (var provider in _providers)
{
EntityId = x.taskSection.Id,
Title = "برگشت از سمت مدیر",
Type = WorkflowType.Rejected
}).ToList();
var res = new WorkflowListResponse(workflowListItems);
var providerItems = await provider.GetItems(currentUserId, _context, cancellationToken);
if (providerItems?.Count > 0)
items.AddRange(providerItems);
}
var res = new WorkflowListResponse(items);
return OperationResult<WorkflowListResponse>.Success(res);
}
}

View File

@@ -0,0 +1,23 @@
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
namespace GozareshgirProgramManager.Application._Common.Extensions;
public static class FileExtensions
{
public static UploadedFileDto ToDto(this UploadedFile file)
{
return new UploadedFileDto()
{
Id = file.Id,
FileName = file.OriginalFileName,
FileUrl = file.StorageUrl ?? "",
FileSizeBytes = file.FileSizeBytes,
FileType = file.FileType.ToString(),
ThumbnailUrl = file.ThumbnailUrl,
ImageWidth = file.ImageWidth,
ImageHeight = file.ImageHeight,
DurationSeconds = file.DurationSeconds
};
}
}

View File

@@ -0,0 +1,34 @@
namespace GozareshgirProgramManager.Application._Common.Models;
public class UploadedFileDto
{
public Guid Id { get; set; }
public string FileName { get; set; } = string.Empty;
public string FileUrl { get; set; } = string.Empty;
public long FileSizeBytes { get; set; }
public string FileType { get; set; } = string.Empty;
public string? ThumbnailUrl { get; set; }
public int? ImageWidth { get; set; }
public int? ImageHeight { get; set; }
public int? DurationSeconds { get; set; }
public string FileSizeFormatted
{
get
{
const long kb = 1024;
const long mb = kb * 1024;
const long gb = mb * 1024;
if (FileSizeBytes >= gb)
return $"{FileSizeBytes / (double)gb:F2} GB";
if (FileSizeBytes >= mb)
return $"{FileSizeBytes / (double)mb:F2} MB";
if (FileSizeBytes >= kb)
return $"{FileSizeBytes / (double)kb:F2} KB";
return $"{FileSizeBytes} Bytes";
}
}
}

View File

@@ -30,6 +30,13 @@ public class TaskSectionRevision : EntityBase<Guid>
{
_files.Add(file);
}
public void MarkReviewed()
{
if (Status == RevisionReviewStatus.Reviewed)
return;
Status = RevisionReviewStatus.Reviewed;
}
}
public class TaskRevisionFile: EntityBase<Guid>

View File

@@ -25,5 +25,9 @@ public class TaskSectionTimeRequest:EntityBase<Guid>
public TimeSpan RequestedTime { get; private set; }
public TaskSectionTimeRequestType RequestType { get; private set; }
public TaskSectionTimeRequestStatus RequestStatus { get; private set; }
public void AcceptTimeRequest()
{
RequestStatus = TaskSectionTimeRequestStatus.Accepted;
}
}

View File

@@ -5,5 +5,5 @@ namespace GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
public interface ITaskSectionRevisionRepository:IRepository<Guid,TaskSectionRevision>
{
Task<List<TaskSectionRevision>> GetByTaskSectionId(Guid requestTaskSectionId);
}

View File

@@ -25,6 +25,9 @@ using Shared.Contracts.PmRole.Commands;
using Shared.Contracts.PmRole.Queries;
using Shared.Contracts.PmUser.Commands;
using Shared.Contracts.PmUser.Queries;
using System.Reflection;
using GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList;
using GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList.Providers;
namespace GozareshgirProgramManager.Infrastructure;
@@ -114,6 +117,16 @@ public static class DependencyInjection
// MediatR Validation Behavior
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
// Workflow providers: auto-register all IWorkflowProvider implementations in the Application assembly
var appAssembly = typeof(IWorkflowProvider).Assembly;
var providerTypes = appAssembly
.GetTypes()
.Where(t => !t.IsAbstract && !t.IsInterface && typeof(IWorkflowProvider).IsAssignableFrom(t))
.ToList();
foreach (var providerType in providerTypes)
{
services.AddScoped(typeof(IWorkflowProvider), providerType);
}
return services;
}

View File

@@ -13,4 +13,12 @@ public class TaskSectionRevisionRepository:RepositoryBase<Guid,TaskSectionRevisi
{
_programManagerDbContext = programManagerDbContext;
}
public async Task<List<TaskSectionRevision>> GetByTaskSectionId(Guid requestTaskSectionId)
{
var res = await _programManagerDbContext.TaskSectionRevisions
.Where(x => requestTaskSectionId == x.TaskSectionId)
.ToListAsync();
return res;
}
}

View File

@@ -1,6 +1,8 @@
using System.Runtime.InteropServices;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.TaskSectionTimeRequests.Commands.AcceptTimeRequest;
using GozareshgirProgramManager.Application.Modules.TaskSectionTimeRequests.Commands.CreateTimeRequest;
using GozareshgirProgramManager.Application.Modules.TaskSectionTimeRequests.Queries.CreateTimeRequestDetails;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using MediatR;
using Microsoft.AspNetCore.Mvc;
@@ -17,6 +19,13 @@ public class TimeRequestController:ProgramManagerBaseController
_mediator = mediator;
}
[HttpGet("Rejected")]
public async Task<ActionResult<OperationResult>> GetCreateRejectedTimeRequest(Guid taskSectionId)
{
var command = new CreateTimeRequestDetailsQuery(taskSectionId);
var res = await _mediator.Send(command);
return res;
}
[HttpPost("Rejected")]
public async Task<ActionResult<OperationResult>> CreateRejectedTimeRequest(CreateTimeRequest request)
{
@@ -43,6 +52,19 @@ public class TimeRequestController:ProgramManagerBaseController
var res = await _mediator.Send(command);
return res;
}
[HttpPost("Accept")]
public async Task<ActionResult<OperationResult>> AcceptTimeRequest(AcceptTimeRequestDto request)
{
var command = new AcceptTimeRequestCommand(request.TimeRequestId, request.SectionId,
request.TimeType, request.Hour, request.Minute);
var res = await _mediator.Send(command);
return res;
}
}
public record CreateTimeRequest(int Hours, int Minutes, string Description,Guid TaskSectionId);
public record CreateTimeRequest(int Hours, int Minutes, string Description,Guid TaskSectionId);
public record AcceptTimeRequestDto(Guid TimeRequestId, Guid SectionId,
TaskSectionAdditionalTimeType TimeType, int Hour, int Minute);

View File

@@ -22,4 +22,11 @@ public class WorkflowController:ProgramManagerBaseController
return res;
}
[HttpGet("count")]
public async Task<ActionResult<OperationResult<WorkflowCountResponse>>> GetCount(WorkflowCountQuery query)
{
var res = await _mediator.Send(query);
return res;
}
}

View File

@@ -19,7 +19,7 @@
"sqlDebugging": true,
"dotnetRunMessages": "true",
"nativeDebugging": true,
"applicationUrl": "https://localhost:5004;http://localhost:5003;",
"applicationUrl": "https://localhost:5004;http://localhost:5003;https://192.168.0.117:5006",
"jsWebView2Debugging": false,
"hotReloadEnabled": true
},
@@ -30,7 +30,7 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation",
"ASPNETCORE_URLS": "https://localhost:5004;http://localhost:5003"
"ASPNETCORE_URLS": "https://localhost:5004;http://localhost:5003;"
},
"distributionName": ""
},
@@ -44,7 +44,7 @@
"sqlDebugging": true,
"dotnetRunMessages": "true",
"nativeDebugging": true,
"applicationUrl": "https://localhost:5004;http://localhost:5003;",
"applicationUrl": "https://localhost:5004;http://localhost:5003;https://192.168.0.117:5006",
"jsWebView2Debugging": false,
"hotReloadEnabled": true
}