merge from ClassificationScheme

This commit is contained in:
SamSys
2026-01-05 15:41:22 +03:30
27 changed files with 733 additions and 90 deletions

View File

@@ -21,6 +21,27 @@ public interface IClassificationGroupRepository : IRepository<long, Classificati
/// <returns></returns>
Task<List<ClassificationGroupAndJobModel>> GetGroupAndJobs(long schemeId);
/// <summary>
/// دریافت لیست گروه ها
/// </summary>
/// <param name="schemeId"></param>
/// <returns></returns>
Task<List<GetGroupAndJobSchemeListDto>> GetGroupList(long schemeId);
/// <summary>
/// دریافت لیست مشاغل برای مودال ایجاد و ویرایش
/// </summary>
/// <param name="groupId"></param>
/// <returns></returns>
Task<AddOrEditJobInGroupDto> GetCreateOrEditJobsData(long groupId);
/// <summary>
/// چک میکند که آی پرسنلی وجود دارد که این شغل به او نسبت داده شده
/// </summary>
/// <param name="jobId"></param>
/// <param name="groupId"></param>
/// <returns></returns>
Task<bool> CheckIfEmployeeHasThisJob(long jobId, long groupId);
/// <summary>
/// دریافت مشاغل گروه توسط آی دی گروه

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
namespace CompanyManagment.App.Contracts.ClassificationScheme;
public class AddOrEditJobInGroupDto
{
/// <summary>
/// آی دی گروه
/// </summary>
public long GroupId { get; set; }
/// <summary>
/// شماره گروه نوع عددی
/// </summary>
public int GroupNoInt { get; set; }
public List<AddJobListDto> AddJobListDto { get; set; }
}
/// <summary>
/// لیست مشغال افزوده شده به گروه
/// </summary>
public class AddJobListDto
{
/// <summary>
/// آی دی شغل در مشاغل اداره کار
/// </summary>
public long JobId { get; set; }
/// <summary>
/// نام شغل
/// </summary>
public string JobName { get; set; }
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace CompanyManagment.App.Contracts.ClassificationScheme;
public class GetGroupAndJobSchemeListDto
{
/// <summary>
/// آی دی گروه
/// </summary>
public long GroupId { get; set; }
/// <summary>
/// شماره گروه نوع عددی
/// </summary>
public int GroupNoInt { get; set; }
/// <summary>
/// آیا شغلی به گروه اضافه شده
/// </summary>
public bool HasAnyJob { get; set; }
}

View File

@@ -185,12 +185,14 @@ public interface IClassificationSchemeApplication
Task<BaseYearDataViewModel> BaseYearComputeOneGroup(DateTime schemeStart, DateTime? schemeEnd,
DateTime contractStart, DateTime contractEnd, string groupNo, long employeeId, long workshopId);
#region ForApi
/// <summary>
/// چک کردن امکان حذف طرح
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task<CheckStatusToDeleteScheme> CheckToDeleteScheme(long id);
Task<OperationResult<CheckStatusToDeleteScheme>> CheckToDeleteScheme(long id);
/// <summary>
/// حذف طرح
@@ -199,4 +201,30 @@ public interface IClassificationSchemeApplication
/// <returns></returns>
Task<OperationResult> DeleteScheme(long id);
/// <summary>
/// تب گروه ها و مشاغل
/// لیست گروه ها
/// </summary>
/// <param name="schemeId"></param>
/// <returns></returns>
Task<List<GetGroupAndJobSchemeListDto>> GetGroupList(long schemeId);
/// <summary>
/// دریافت لیست مشاغل برای مودال ایجاد و ویرایش
/// </summary>
/// <param name="groupId"></param>
/// <returns></returns>
Task<AddOrEditJobInGroupDto> GetCreateOrEditJobsData(long groupId);
/// <summary>
/// چک میکند که آی پرسنلی وجود دارد که این شغل به او نسبت داده شده
/// </summary>
/// <param name="jobId"></param>
/// <param name="groupId"></param>
/// <returns></returns>
Task<OperationResult> CheckIfEmployeeHasThisJob(long jobId, long groupId);
#endregion
}

View File

@@ -132,11 +132,18 @@ public class ClassificationSchemeApplication : IClassificationSchemeApplication
return await _classificationGroupRepository.GetGroupAndJobs(schemeId);
}
public async Task<List<GetGroupAndJobSchemeListDto>> GetGroupList(long schemeId)
{
return await _classificationGroupRepository.GetGroupList(schemeId);
}
public async Task<List<EditClassificationGroupJob>> GetGroupJobs(long groupId)
{
return await _classificationGroupRepository.GetGroupJobs(groupId);
}
public async Task<bool> CheckEmployeeHasThisJob(long id, long groupId)
{
return await _classificationGroupRepository.CheckEmployeeHasThisJob(id, groupId);
@@ -437,7 +444,7 @@ public class ClassificationSchemeApplication : IClassificationSchemeApplication
edit.EditMultipleGroupMember(lastRecord.ClassificationGroupId, lastRecord.ClassificationGroupJobId, lastRecord.StartGroupDate.Value);
}
newStartDateList = newStartDateList.OrderByDescending(x => x).ToList();
@@ -480,7 +487,7 @@ public class ClassificationSchemeApplication : IClassificationSchemeApplication
var zeroItem = command.First(x => x.Id == 0);
var oldGroupsMemberize = await _classificationEmployeeRepository.GetEmployeeMemberizeData(zeroItem.EmployeeId);
var scheme = await _classificationSchemeRepository.GetClassificationSchemeToCompute(zeroItem.SchemeId);
command = command.Where(x=>x.Id != 0).ToList();
command = command.Where(x => x.Id != 0).ToList();
var newStartDateList = new List<DateTime>();
foreach (var item in command)
{
@@ -500,7 +507,7 @@ public class ClassificationSchemeApplication : IClassificationSchemeApplication
if (item.StartGroupDateFa.Substring(8, 2) != "01")
return op.Failed("تاریخ شروع فقط می تواند یکم هر ماه باشد");
if (oldGroupsMemberize.Any(x=>x.StartGroupDate == startDate && x.Id != item.Id))
if (oldGroupsMemberize.Any(x => x.StartGroupDate == startDate && x.Id != item.Id))
return op.Failed($"تاریخ {item.StartGroupDateFa} با تاریخ های قبل یا بعد از خود تداخل دارد");
if (startDate < scheme.ExecutionDateGr)
@@ -518,14 +525,14 @@ public class ClassificationSchemeApplication : IClassificationSchemeApplication
}
var removeItemIdList = command.Select(x => x.Id).ToList();
var toBeRemove = removeItemIdList.Any() ? oldGroupsMemberize.Where(x=> !removeItemIdList.Contains(x.Id)).Select(x=>x.Id).ToList() : oldGroupsMemberize.Select(x=>x.Id).ToList();
var toBeRemove = removeItemIdList.Any() ? oldGroupsMemberize.Where(x => !removeItemIdList.Contains(x.Id)).Select(x => x.Id).ToList() : oldGroupsMemberize.Select(x => x.Id).ToList();
if (toBeRemove.Any())
{
var getRemoveList =await _classificationEmployeeRepository.GetListByIdList(toBeRemove);
var getRemoveList = await _classificationEmployeeRepository.GetListByIdList(toBeRemove);
await _classificationEmployeeRepository.RemoveRangeByEdit(getRemoveList);
}
@@ -563,35 +570,38 @@ public class ClassificationSchemeApplication : IClassificationSchemeApplication
contractEnd, groupNo, employeeId, workshopId);
}
public async Task<CheckStatusToDeleteScheme> CheckToDeleteScheme(long id)
public async Task<OperationResult<CheckStatusToDeleteScheme>> CheckToDeleteScheme(long id)
{
var op = new CheckStatusToDeleteScheme();
var op = new OperationResult<CheckStatusToDeleteScheme>();
var scheme = _classificationSchemeRepository.Get(id);
if (scheme != null)
{
var employeeInfoList =await _classificationEmployeeRepository.GetEmployeeListData(id);
var employeeInfoList = await _classificationEmployeeRepository.GetEmployeeListData(id);
var anyHasGroup = employeeInfoList.Any(x => x.HasGroup);
if (employeeInfoList.Any() && anyHasGroup)
{
op.DeleteSchemeStatus = DeleteSchemeStatus.ConfirmationNeeded;
op.Message = "برای این طرح پرسنل افزوده شده است، آیا از حذف طرح اطمینان دارید";
return op;
string message = "برای این طرح پرسنل افزوده شده است، آیا از حذف طرح اطمینان دارید";
return op.Succcedded(new CheckStatusToDeleteScheme()
{ DeleteSchemeStatus = DeleteSchemeStatus.ConfirmationNeeded, Message = message });
}
else
{
op.DeleteSchemeStatus = DeleteSchemeStatus.Valid;
op.Message = "مجاز برای حذف";
return op;
var message = "مجاز برای حذف";
return op.Succcedded(new CheckStatusToDeleteScheme()
{ DeleteSchemeStatus = DeleteSchemeStatus.Valid, Message = message });
}
}
op.DeleteSchemeStatus = DeleteSchemeStatus.NotValid;
op.Message = "یافت نشد";
return op;
return op.Failed("یافت نشد", new CheckStatusToDeleteScheme() { DeleteSchemeStatus = DeleteSchemeStatus.NotValid });
}
public async Task<OperationResult> DeleteScheme(long id)
@@ -607,4 +617,20 @@ public class ClassificationSchemeApplication : IClassificationSchemeApplication
return op.Failed("یافت نشد");
}
public async Task<AddOrEditJobInGroupDto> GetCreateOrEditJobsData(long groupId)
{
return await _classificationGroupRepository.GetCreateOrEditJobsData(groupId);
}
public async Task<OperationResult> CheckIfEmployeeHasThisJob(long jobId, long groupId)
{
var op = new OperationResult();
var checkExistAny = await _classificationGroupRepository.CheckIfEmployeeHasThisJob(jobId, groupId);
if (checkExistAny)
return op.Failed("این شغل قبلا به پرسنلی از این گروه داده شده و نمیتوانید آن را حذف کنید");
return op.Succcedded(-1, "حذف با موفقیت انجام شد");
}
}

View File

@@ -407,6 +407,10 @@ public class WorkshopAppliction : IWorkshopApplication
public EditWorkshop GetDetails(long id)
{
var workshop = _workshopRepository.GetDetails(id);
if (workshop == null)
{
return null;
}
if (workshop.IsClassified)
{
workshop.CreatePlan = _workshopPlanApplication.GetWorkshopPlanByWorkshopId(id);

View File

@@ -65,6 +65,8 @@ public class ClassificationGroupRepository : RepositoryBase<long, Classification
}).OrderBy(x=>x.GroupNoInt).ToListAsync();
}
/// <summary>
/// دریافت مشاغل گروه توسط آی دی گروه
/// </summary>
@@ -164,4 +166,63 @@ public class ClassificationGroupRepository : RepositoryBase<long, Classification
await _context.AddRangeAsync(groupList);
await _context.SaveChangesAsync();
}
#region ForApi
public async Task<List<GetGroupAndJobSchemeListDto>> GetGroupList(long schemeId)
{
return await _context.ClassificationGroups.Where(x => x.ClassificationSchemeId == schemeId)
.Include(x => x.ClassificationGroupJobs).Select(x => new GetGroupAndJobSchemeListDto
{
GroupId = x.id,
GroupNoInt = Convert.ToInt32(x.GroupNo),
HasAnyJob = x.ClassificationGroupJobs.Any()
}).OrderBy(x => x.GroupNoInt).ToListAsync();
}
public async Task<AddOrEditJobInGroupDto> GetCreateOrEditJobsData(long groupId)
{
var result = await _context.ClassificationGroups.Where(x => x.id == groupId)
.Include(x => x.ClassificationGroupJobs).FirstOrDefaultAsync();
if (result == null)
return new AddOrEditJobInGroupDto();
return new AddOrEditJobInGroupDto()
{
GroupId = result.id,
GroupNoInt = Convert.ToInt32(result.GroupNo),
AddJobListDto = result.ClassificationGroupJobs.Select(job => new AddJobListDto
{
JobId = job.JobId,
JobName = $"{job.JobName} - {job.JobCode}"
}).ToList(),
};
}
/// <summary>
/// چک میکند که آی پرسنلی وجود دارد که این شغل به او نسبت داده شده
/// </summary>
/// <param name="jobId"></param>
/// <param name="groupId"></param>
/// <returns></returns>
public async Task<bool> CheckIfEmployeeHasThisJob(long jobId, long groupId)
{
var result = await _context.ClassificationGroups.Where(x => x.id == groupId)
.Include(x => x.ClassificationGroupJobs).FirstOrDefaultAsync();
var job = result.ClassificationGroupJobs.FirstOrDefault(x => x.JobId == jobId);
if (job == null)
return false;
var id = job.id;
return await _context.ClassificationEmployees.AnyAsync(x =>
x.ClassificationGroupJobId == id && x.ClassificationGroupId == groupId);
}
#endregion
}

View File

@@ -0,0 +1,57 @@
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.ApproveTaskSectionCompletion;
public record ApproveTaskSectionCompletionCommand(Guid TaskSectionId, bool IsApproved) : IBaseCommand;
public class ApproveTaskSectionCompletionCommandHandler : IBaseCommandHandler<ApproveTaskSectionCompletionCommand>
{
private readonly ITaskSectionRepository _taskSectionRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IAuthHelper _authHelper;
public ApproveTaskSectionCompletionCommandHandler(
ITaskSectionRepository taskSectionRepository,
IUnitOfWork unitOfWork,
IAuthHelper authHelper)
{
_taskSectionRepository = taskSectionRepository;
_unitOfWork = unitOfWork;
_authHelper = authHelper;
}
public async Task<OperationResult> Handle(ApproveTaskSectionCompletionCommand request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId()
?? throw new UnAuthorizedException("˜ÇÑÈÑ ÇÍÑÇÒ åæ?Ê äÔÏå ÇÓÊ");
var section = await _taskSectionRepository.GetByIdAsync(request.TaskSectionId, cancellationToken);
if (section == null)
{
return OperationResult.NotFound("ÈÎÔ ãæÑÏ äÙÑ ?ÇÝÊ äÔÏ");
}
if (section.Status != TaskSectionStatus.PendingForCompletion)
{
return OperationResult.Failure("ÝÞØ ÈÎÔ<C38E>åÇ?? ˜å ÏÑ ÇäÊÙÇÑ Ê˜ã?á åÓÊäÏ ÞÇÈá ÊÇ??Ï ?Ç ÑÏ åÓÊäÏ");
}
if (request.IsApproved)
{
section.UpdateStatus(TaskSectionStatus.Completed);
}
else
{
section.UpdateStatus(TaskSectionStatus.Incomplete);
}
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
}

View File

@@ -0,0 +1,14 @@
using FluentValidation;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion;
public class ApproveTaskSectionCompletionCommandValidator : AbstractValidator<ApproveTaskSectionCompletionCommand>
{
public ApproveTaskSectionCompletionCommandValidator()
{
RuleFor(c => c.TaskSectionId)
.NotEmpty()
.NotNull()
.WithMessage("ÔäÇÓå ÈÎÔ äã?<3F>ÊæÇäÏ ÎÇá? ÈÇÔÏ");
}
}

View File

@@ -52,7 +52,10 @@ public class ChangeStatusSectionCommandHandler : IBaseCommandHandler<ChangeStatu
// 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
@@ -86,9 +89,9 @@ public class ChangeStatusSectionCommandHandler : IBaseCommandHandler<ChangeStatu
var validTransitions = new Dictionary<TaskSectionStatus, List<TaskSectionStatus>>
{
{ TaskSectionStatus.ReadyToStart, [TaskSectionStatus.InProgress] },
{ TaskSectionStatus.InProgress, [TaskSectionStatus.Incomplete, TaskSectionStatus.Completed] },
{ TaskSectionStatus.Incomplete, [TaskSectionStatus.InProgress, TaskSectionStatus.Completed] },
{ TaskSectionStatus.Completed, [TaskSectionStatus.InProgress, TaskSectionStatus.Incomplete] }, // Can return to InProgress or Incomplete
{ 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] }
};

View File

@@ -72,6 +72,17 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
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)
{
@@ -102,6 +113,16 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
// اگر 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 )
{
@@ -122,6 +143,16 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
// اگر 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);
@@ -177,6 +208,18 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
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)
{
@@ -200,6 +243,16 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
// اگر 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);
@@ -246,7 +299,16 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
var validSkills = request.SkillItems
.Where(x=>x.UserId is > 0).ToList();
task.ClearTaskSections();
// حذف سکشن‌هایی که در 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)
{
@@ -304,7 +366,7 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
// افزودن زمان‌های اضافی
foreach (var additionalTime in sectionItem.AdditionalTime)
{
var additionalTimeSpan = TimeSpan.FromHours(additionalTime.Hours);
var additionalTimeSpan = TimeSpan.FromHours(additionalTime.Hours).Add(TimeSpan.FromMinutes(additionalTime.Minutes));
section.AddAdditionalTime(additionalTimeSpan, additionalTime.Description, addedByUserId);
}
}

View File

@@ -0,0 +1,11 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
/// <summary>
/// درخواست جستجو در سراسر سلسله‌مراتب پروژه (پروژه، فاز، تسک).
/// نتایج با اطلاعات مسیر سلسله‌مراتب برای پشتیبانی از ناوبری درخت در رابط کاربری بازگردانده می‌شود.
/// </summary>
public record GetProjectSearchQuery(
string SearchQuery) : IBaseQuery<GetProjectSearchResponse>;

View File

@@ -0,0 +1,132 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
/// <summary>
/// Handler برای درخواست جستجوی سراسری در سلسله‌مراتب پروژه.
/// این handler در تمام سطح‌های پروژه، فاز و تسک جستجو می‌کند و از تمام فیلدهای متنی (نام، توضیحات) استفاده می‌کند.
/// همچنین در زیرمجموعه‌های هر سطح (ProjectSections، PhaseSections، TaskSections) جستجو می‌کند.
/// </summary>
public class GetProjectSearchQueryHandler : IBaseQueryHandler<GetProjectSearchQuery, GetProjectSearchResponse>
{
private readonly IProgramManagerDbContext _context;
private const int MaxResults = 50;
public GetProjectSearchQueryHandler(IProgramManagerDbContext context)
{
_context = context;
}
public async Task<OperationResult<GetProjectSearchResponse>> Handle(
GetProjectSearchQuery request,
CancellationToken cancellationToken)
{
var searchQuery = request.SearchQuery.ToLower();
var results = new List<ProjectHierarchySearchResultDto>();
// جستجو در پروژه‌ها و ProjectSections
var projects = await SearchProjects(searchQuery, cancellationToken);
results.AddRange(projects);
// جستجو در فازها و PhaseSections
var phases = await SearchPhases(searchQuery, cancellationToken);
results.AddRange(phases);
// جستجو در تسک‌ها و TaskSections
var tasks = await SearchTasks(searchQuery, cancellationToken);
results.AddRange(tasks);
// مرتب‌سازی نتایج: ابتدا بر اساس سطح سلسله‌مراتب (پروژه → فاز → تسک)، سپس بر اساس نام
var sortedResults = results
.OrderBy(r => r.Level)
.ThenBy(r => r.Title)
.Take(MaxResults)
.ToList();
var response = new GetProjectSearchResponse(sortedResults);
return OperationResult<GetProjectSearchResponse>.Success(response);
}
/// <summary>
/// جستجو در جدول پروژه‌ها (نام، توضیحات) و ProjectSections (نام مهارت، توضیحات اولیه)
/// </summary>
private async Task<List<ProjectHierarchySearchResultDto>> SearchProjects(
string searchQuery,
CancellationToken cancellationToken)
{
var projects = await _context.Projects
.Where(p =>
p.Name.ToLower().Contains(searchQuery) ||
(p.Description != null && p.Description.ToLower().Contains(searchQuery)))
.Select(p => new ProjectHierarchySearchResultDto
{
Id = p.Id,
Title = p.Name,
Level = ProjectHierarchyLevel.Project,
ProjectId = null,
PhaseId = null
})
.ToListAsync(cancellationToken);
return projects;
}
/// <summary>
/// جستجو در جدول فازهای پروژه (نام، توضیحات) و PhaseSections
/// </summary>
private async Task<List<ProjectHierarchySearchResultDto>> SearchPhases(
string searchQuery,
CancellationToken cancellationToken)
{
var phases = await _context.ProjectPhases
.Where(ph =>
ph.Name.ToLower().Contains(searchQuery) ||
(ph.Description != null && ph.Description.ToLower().Contains(searchQuery)))
.Select(ph => new ProjectHierarchySearchResultDto
{
Id = ph.Id,
Title = ph.Name,
Level = ProjectHierarchyLevel.Phase,
ProjectId = ph.ProjectId,
PhaseId = null
})
.ToListAsync(cancellationToken);
return phases;
}
/// <summary>
/// جستجو در جدول تسک‌های پروژه (نام، توضیحات) و TaskSections (نام مهارت، توضیح اولیه، اطلاعات اضافی)
/// </summary>
private async Task<List<ProjectHierarchySearchResultDto>> SearchTasks(
string searchQuery,
CancellationToken cancellationToken)
{
var tasks = await _context.ProjectTasks
.Include(t => t.Sections)
.Include(t => t.Phase)
.Where(t =>
t.Name.ToLower().Contains(searchQuery) ||
(t.Description != null && t.Description.ToLower().Contains(searchQuery)) ||
t.Sections.Any(s =>
(s.InitialDescription != null && s.InitialDescription.ToLower().Contains(searchQuery)) ||
s.AdditionalTimes.Any(at => at.Reason != null && at.Reason.ToLower().Contains(searchQuery))))
.Select(t => new ProjectHierarchySearchResultDto
{
Id = t.Id,
Title = t.Name,
Level = ProjectHierarchyLevel.Task,
ProjectId = t.Phase.ProjectId,
PhaseId = t.PhaseId
})
.ToListAsync(cancellationToken);
return tasks;
}
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
/// <summary>
/// اعتبارسنج برای درخواست جستجوی سراسری
/// </summary>
public class GetProjectSearchQueryValidator : AbstractValidator<GetProjectSearchQuery>
{
public GetProjectSearchQueryValidator()
{
RuleFor(x => x.SearchQuery)
.NotEmpty().WithMessage("متن جستجو نمی‌تواند خالی باشد.")
.MinimumLength(2).WithMessage("متن جستجو باید حداقل 2 حرف باشد.")
.MaximumLength(500).WithMessage("متن جستجو نمی‌تواند بیش از 500 حرف باشد.");
}
}

View File

@@ -0,0 +1,8 @@
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
/// <summary>
/// پوسته‌ی پاسخ برای نتایج جستجوی سراسری
/// </summary>
public record GetProjectSearchResponse(
List<ProjectHierarchySearchResultDto> Results);

View File

@@ -0,0 +1,36 @@
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
/// <summary>
/// DTO برای نتایج جستجوی سراسری در سلسله‌مراتب پروژه.
/// حاوی اطلاعات کافی برای بازسازی مسیر سلسله‌مراتب و بسط درخت در رابط کاربری است.
/// </summary>
public record ProjectHierarchySearchResultDto
{
/// <summary>
/// شناسه آیتم (پروژه، فاز یا تسک)
/// </summary>
public Guid Id { get; init; }
/// <summary>
/// نام/عنوان آیتم
/// </summary>
public string Title { get; init; } = string.Empty;
/// <summary>
/// سطح سلسله‌مراتب این آیتم
/// </summary>
public ProjectHierarchyLevel Level { get; init; }
/// <summary>
/// شناسه پروژه - همیشه برای فاز و تسک پر شده است، برای پروژه با شناسه خود پر می‌شود
/// </summary>
public Guid? ProjectId { get; init; }
/// <summary>
/// شناسه فاز - فقط برای تسک پر شده است، برای پروژه و فاز خالی است
/// </summary>
public Guid? PhaseId { get; init; }
}

View File

@@ -53,67 +53,81 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
.ToDictionaryAsync(x => x.Id, x => x.FullName, cancellationToken);
var result = data.Select(x =>
{
// محاسبه یکبار برای هر Activity و Cache کردن نتیجه
var activityTimeData = x.Activities.Select(a =>
var result = data
.Select(x =>
{
var timeSpent = a.GetTimeSpent();
return new
// محاسبه یکبار برای هر Activity و Cache کردن نتیجه
var activityTimeData = x.Activities.Select(a =>
{
Activity = a,
TimeSpent = timeSpent,
TotalSeconds = timeSpent.TotalSeconds,
FormattedTime = timeSpent.ToString(@"hh\:mm")
};
}).ToList();
// ادغام پشت سر هم فعالیت‌های یک کاربر
var mergedHistories = new List<ProjectProgressHistoryDto>();
foreach (var activityData in activityTimeData)
{
var lastHistory = mergedHistories.LastOrDefault();
// اگر آخرین history برای همین کاربر باشد، زمان‌ها را جمع می‌کنیم
if (lastHistory != null && lastHistory.UserId == activityData.Activity.UserId)
{
var totalTimeSpan = lastHistory.WorkedTimeSpan + activityData.TimeSpent;
lastHistory.WorkedTimeSpan = totalTimeSpan;
lastHistory.WorkedTime = totalTimeSpan.ToString(@"hh\:mm");
}
else
{
// در غیر این صورت، یک history جدید اضافه می‌کنیم
mergedHistories.Add(new ProjectProgressHistoryDto()
var timeSpent = a.GetTimeSpent();
return new
{
UserId = activityData.Activity.UserId,
IsCurrentUser = activityData.Activity.UserId == currentUserId,
Name = users.GetValueOrDefault(activityData.Activity.UserId, "ناشناس"),
WorkedTime = activityData.FormattedTime,
WorkedTimeSpan = activityData.TimeSpent,
});
}
}
Activity = a,
TimeSpent = timeSpent,
TotalSeconds = timeSpent.TotalSeconds,
FormattedTime = timeSpent.ToString(@"hh\:mm")
};
}).ToList();
return new ProjectBoardListResponse()
{
Id = x.Id,
PhaseName = x.Task.Phase.Name,
ProjectName = x.Task.Phase.Project.Name,
TaskName = x.Task.Name,
SectionStatus = x.Status,
Progress = new ProjectProgressDto()
// ادغام پشت سر هم فعالیت‌های یک کاربر
var mergedHistories = new List<ProjectProgressHistoryDto>();
foreach (var activityData in activityTimeData)
{
CompleteSecond = x.FinalEstimatedHours.TotalSeconds,
CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds),
Histories = mergedHistories
},
OriginalUser = users.GetValueOrDefault(x.OriginalAssignedUserId, "ناشناس"),
AssignedUser = x.CurrentAssignedUserId == x.OriginalAssignedUserId ? null
: users.GetValueOrDefault(x.CurrentAssignedUserId, "ناشناس"),
SkillName = x.Skill?.Name??"-",
};
}).ToList();
var lastHistory = mergedHistories.LastOrDefault();
// اگر آخرین history برای همین کاربر باشد، زمان‌ها را جمع می‌کنیم
if (lastHistory != null && lastHistory.UserId == activityData.Activity.UserId)
{
var totalTimeSpan = lastHistory.WorkedTimeSpan + activityData.TimeSpent;
lastHistory.WorkedTimeSpan = totalTimeSpan;
lastHistory.WorkedTime = totalTimeSpan.ToString(@"hh\:mm");
}
else
{
// در غیر این صورت، یک history جدید اضافه می‌کنیم
mergedHistories.Add(new ProjectProgressHistoryDto()
{
UserId = activityData.Activity.UserId,
IsCurrentUser = activityData.Activity.UserId == currentUserId,
Name = users.GetValueOrDefault(activityData.Activity.UserId, "ناشناس"),
WorkedTime = activityData.FormattedTime,
WorkedTimeSpan = activityData.TimeSpent,
});
}
}
mergedHistories = mergedHistories.OrderByDescending(h => h.IsCurrentUser).ToList();
return new ProjectBoardListResponse()
{
Id = x.Id,
PhaseName = x.Task.Phase.Name,
ProjectName = x.Task.Phase.Project.Name,
TaskName = x.Task.Name,
SectionStatus = x.Status,
Progress = new ProjectProgressDto()
{
CompleteSecond = x.FinalEstimatedHours.TotalSeconds,
CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds),
Histories = mergedHistories
},
OriginalUser = users.GetValueOrDefault(x.OriginalAssignedUserId, "ناشناس"),
AssignedUser = x.CurrentAssignedUserId == x.OriginalAssignedUserId ? null
: users.GetValueOrDefault(x.CurrentAssignedUserId, "ناشناس"),
SkillName = x.Skill?.Name??"-",
};
})
.OrderByDescending(r =>
{
// اگر AssignedUser null نباشد، بررسی کن که برابر current user هست یا نه
if (r.AssignedUser != null)
{
return users.FirstOrDefault(u => u.Value == r.AssignedUser).Key == currentUserId;
}
// اگر AssignedUser null بود، از OriginalUser بررسی کن
return users.FirstOrDefault(u => u.Value == r.OriginalUser).Key == currentUserId;
})
.ToList();
return OperationResult<List<ProjectBoardListResponse>>.Success(result);
}

View File

@@ -75,14 +75,14 @@ public class ProjectSetTimeDetailsQueryHandler
CreationDate = x.CreationDate.ToFarsi()
}).OrderBy(x => x.CreationDate).ToList() ?? [],
InitCreationTime = section?.CreationDate.ToFarsi() ?? "",
SkillName = skill?.Name ?? "",
SkillName = skill.Name ?? "",
UserFullName = user?.FullName ?? "",
SectionId = section?.Id ?? Guid.Empty,
InitialDescription = section?.InitialDescription ?? "",
InitialHours = (int)(section?.InitialEstimatedHours.TotalHours ?? 0),
InitialMinutes = section?.InitialEstimatedHours.Minutes ?? 0,
UserId = section?.OriginalAssignedUserId ?? 0,
SkillId = task.Id,
SkillId = skill.Id,
};
}).OrderBy(x => x.SkillId).ToList(),
task.Id,

View File

@@ -1,4 +1,4 @@
using FluentValidation;
using FluentValidation;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails;

View File

@@ -74,6 +74,15 @@ public class Project : ProjectHierarchyNode
}
}
public void RemoveProjectSection(Guid skillId)
{
var section = _projectSections.FirstOrDefault(s => s.SkillId == skillId);
if (section != null)
{
_projectSections.Remove(section);
}
}
public void ClearProjectSections()
{
_projectSections.Clear();

View File

@@ -87,6 +87,15 @@ public class ProjectPhase : ProjectHierarchyNode
}
}
public void RemovePhaseSection(Guid skillId)
{
var section = _phaseSections.FirstOrDefault(s => s.SkillId == skillId);
if (section != null)
{
_phaseSections.Remove(section);
}
}
public void ClearPhaseSections()
{
_phaseSections.Clear();

View File

@@ -13,4 +13,5 @@ public interface ITaskSectionRepository: IRepository<Guid,TaskSection>
Task<List<TaskSection>> GetAssignedToUserAsync(long userId);
Task<List<TaskSection>> GetActiveSectionsIncludeAllAsync(CancellationToken cancellationToken);
Task<bool> HasUserAnyInProgressSectionAsync(long userId, CancellationToken cancellationToken = default);
}

View File

@@ -19,6 +19,7 @@ public class ProjectPhaseRepository : RepositoryBase<Guid, ProjectPhase>, IProje
public Task<ProjectPhase?> GetWithTasksAsync(Guid phaseId)
{
return _context.ProjectPhases
.Include(x=>x.PhaseSections)
.Include(p => p.Tasks)
.ThenInclude(t => t.Sections)
.ThenInclude(s => s.Skill)

View File

@@ -45,4 +45,11 @@ public class TaskSectionRepository:RepositoryBase<Guid,TaskSection>,ITaskSection
.Include(x => x.AdditionalTimes)
.ToListAsync(cancellationToken);
}
public async Task<bool> HasUserAnyInProgressSectionAsync(long userId, CancellationToken cancellationToken = default)
{
return await _context.TaskSections
.AnyAsync(x => x.CurrentAssignedUserId == userId && x.Status == TaskSectionStatus.InProgress,
cancellationToken);
}
}

View File

@@ -21,6 +21,8 @@ public class ClassificationSchemeController : AdminBaseController
_authHelper = authHelper;
}
#region SchemeTab
/// <summary>
/// لیست طرح
/// </summary>
@@ -29,7 +31,7 @@ public class ClassificationSchemeController : AdminBaseController
[HttpGet]
public async Task<ActionResult<ClassificationSchemeListDto>> GetList(long workshopId)
{
var result =await _classificationSchemeApplication.GetClassificationSchemeList(workshopId);
var result = await _classificationSchemeApplication.GetClassificationSchemeList(workshopId);
return result;
}
@@ -66,7 +68,7 @@ public class ClassificationSchemeController : AdminBaseController
[HttpPut("Scheme")]
public async Task<ActionResult<OperationResult>> EditScheme([FromBody] EditClassificationSchemeDto command)
{
var result =await _classificationSchemeApplication.EditClassificationScheme(command);
var result = await _classificationSchemeApplication.EditClassificationScheme(command);
return result;
}
@@ -76,9 +78,9 @@ public class ClassificationSchemeController : AdminBaseController
/// <param name="id"></param>
/// <returns></returns>
[HttpGet("CheckToDeleteScheme")]
public async Task<CheckStatusToDeleteScheme> CheckToDeleteScheme(long id)
public async Task<OperationResult<CheckStatusToDeleteScheme>> CheckToDeleteScheme(long id)
{
var result =await _classificationSchemeApplication.CheckToDeleteScheme(id);
var result = await _classificationSchemeApplication.CheckToDeleteScheme(id);
return result;
}
@@ -93,4 +95,47 @@ public class ClassificationSchemeController : AdminBaseController
var result = await _classificationSchemeApplication.DeleteScheme(id);
return result;
}
#endregion
#region GroupsTab
/// <summary>
/// دریافت لیست گروه ها
/// </summary>
/// <param name="schemeId"></param>
/// <returns></returns>
[HttpGet("GetGroupList")]
public async Task<List<GetGroupAndJobSchemeListDto>> GetGroupList(long schemeId)
{
var result = await _classificationSchemeApplication.GetGroupList(schemeId);
return result;
}
/// <summary>
/// دریافت لیست مشاغل گروه برای مودال افزودن و ویرایش
/// </summary>
/// <param name="groupId"></param>
/// <returns></returns>
[HttpGet("GetCreateOrEditJobsData")]
public async Task<AddOrEditJobInGroupDto> GetCreateOrEditJobsData(long groupId)
{
var result = await _classificationSchemeApplication.GetCreateOrEditJobsData(groupId);
return result;
}
/// <summary>
/// چک میکند که آیا امکان حذف شغل از گروه وجود دارد
/// </summary>
/// <param name="jobId"></param>
/// <param name="groupId"></param>
/// <returns></returns>
[HttpGet("CheckDeleteJobFromGroup")]
public async Task<ActionResult<OperationResult>> CheckDeleteJobFromGroup(long jobId, long groupId)
{
var result = await _classificationSchemeApplication.CheckIfEmployeeHasThisJob(jobId, groupId);
return result;
}
#endregion
}

View File

@@ -1,5 +1,6 @@
using System.Runtime.InteropServices;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AssignProject;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoStopOverTimeTaskSections;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoUpdateDeployStatus;
@@ -17,6 +18,7 @@ using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectBoar
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardDetail;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardList;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ServiceHost.BaseControllers;
@@ -40,6 +42,15 @@ public class ProjectController : ProgramManagerBaseController
return res;
}
[HttpGet("search")]
public async Task<ActionResult<OperationResult<GetProjectSearchResponse>>> Search(
[FromQuery] string query)
{
var searchQuery = new GetProjectSearchQuery(query);
var res = await _mediator.Send(searchQuery);
return res;
}
[HttpPost]
public async Task<ActionResult<OperationResult>> Create([FromBody] CreateProjectCommand command)
{
@@ -147,4 +158,11 @@ public class ProjectController : ProgramManagerBaseController
var res = await _mediator.Send(command);
return res;
}
[HttpPost("approve-completion")]
public async Task<ActionResult<OperationResult>> ApproveTaskSectionCompletion([FromBody] ApproveTaskSectionCompletionCommand command)
{
var res = await _mediator.Send(command);
return res;
}
}

View File

@@ -80,7 +80,7 @@ namespace ServiceHost.Areas.AdminNew.Pages.Company.Ticket
public IActionResult OnGetShowDetailTicketByAdmin(long ticketID)
{
var res = _ticketApplication.GetDetails(ticketID);
res.WorkshopName = _workshopApplication.GetDetails(res.WorkshopId).WorkshopFullName;
res.WorkshopName = _workshopApplication.GetDetails(res.WorkshopId)?.WorkshopFullName??"";
return Partial("DetailTicketModal", res);
}