Compare commits

..

46 Commits

Author SHA1 Message Date
fa4c39904a chnage loan list to new type 2026-02-03 13:44:47 +03:30
0e7787dd56 add page size for search model 2026-02-01 18:14:28 +03:30
87c3cebb60 set workshopId to readonly 2026-01-13 09:38:44 +03:30
607c0780b6 Merge branch 'master' into Feature/loan/client-api 2026-01-10 11:50:52 +03:30
587fa40d81 fix percnetage condition 2026-01-10 10:45:38 +03:30
b741ab9ed2 fix contains no element error for empty skills 2026-01-10 10:34:20 +03:30
b6fde4903a Merge branch 'Feature/institution-contract/sent-to-customer-flag' 2026-01-08 15:03:07 +03:30
0772604432 feat: enhance GetMessagesQuery to include additional time notes in message retrieval 2026-01-08 15:02:43 +03:30
59891d1199 Merge remote-tracking branch 'origin/master' 2026-01-08 14:16:22 +03:30
7cb39b1b92 feat: add UserId filter to ProjectBoardListQuery for enhanced task assignment tracking 2026-01-08 14:16:08 +03:30
SamSys
5580d56874 change logger on program.cs 2026-01-08 14:14:27 +03:30
SamSys
423b49e6e7 Merge branch 'master' of https://github.com/samsyntax24/OriginalGozareshgir 2026-01-08 14:05:30 +03:30
38027352d6 Merge branch 'Feature/program-manager/priority'
# Conflicts:
#	ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListResponse.cs
2026-01-08 14:00:49 +03:30
43562fb49c Merge branch 'Feature/program-manager/chat'
# Conflicts:
#	.gitignore
#	ServiceHost/appsettings.Development.json
#	ServiceHost/appsettings.json
2026-01-08 13:51:06 +03:30
7c611825a4 feat: refactor task priority handling to use ProjectTaskPriority across commands, DTOs, and repositories 2026-01-08 12:09:18 +03:30
ef49302f8a feat: add workshop ID handling in LoanController for loan search filtering 2026-01-08 12:03:54 +03:30
SamSys
bf46dfd1dc Merge branch 'master' of https://github.com/samsyntax24/OriginalGozareshgir 2026-01-08 11:35:10 +03:30
SamSys
a1ed3ad648 logeer change 2026-01-08 11:35:03 +03:30
8679abb1e7 feat: enhance ChangeTaskPriorityCommand to support multi-level priority updates for tasks, phases, and projects 2026-01-08 11:18:15 +03:30
6f076bdc77 feat: update application URL in launchSettings and enhance task sorting by priority in ProjectBoardListQueryHandler 2026-01-08 10:46:04 +03:30
380ed8f6b1 feat: update SetTimeProjectCommandHandler to set status to Incomplete when additional time is added 2026-01-07 18:34:37 +03:30
7423391003 Merge branch 'Feature/program-manager/priority'
# Conflicts:
#	ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs
2026-01-07 18:05:46 +03:30
572f66f905 feat: implement auto-pending for task sections reaching estimated time 2026-01-07 16:52:50 +03:30
140414b866 feat: add SetIsSent endpoint to update contract send status 2026-01-07 16:23:11 +03:30
4ade9e12a6 feat: add InstitutionContractIsSentFlag to track contract send status 2026-01-07 15:03:21 +03:30
dd7e816767 feat: add SetContractSendFlag method and related request for contract send tracking 2026-01-07 14:46:34 +03:30
1deeff996f feat: implement InstitutionContractSendFlag repository and model for contract send tracking 2026-01-07 14:35:17 +03:30
2bea265989 feat: implement task priority change command and update project/task DTOs 2026-01-07 12:11:24 +03:30
ef9b78b924 Merge branch 'refs/heads/master' into Feature/program-manager/priority 2026-01-07 12:01:38 +03:30
8ad296fe61 Merge branch 'Feature/program-manager/fix-bugs' 2026-01-07 11:36:38 +03:30
SamSys
823110ea74 change 2026-01-07 11:24:27 +03:30
061058cbeb fix change status error 2026-01-07 11:21:58 +03:30
c6ed46d8b7 Merge remote-tracking branch 'origin/master' 2026-01-06 21:48:41 +03:30
3da7453ece feat: add assignment status tracking for projects, phases, and tasks 2026-01-06 21:47:22 +03:30
SamSys
9a591fabff change WorningSms methoth 2026-01-06 19:07:54 +03:30
9d09ef60f8 feat: add progress percentage calculations to project and task details 2026-01-06 18:19:16 +03:30
0757ac7e74 Merge branch 'refs/heads/master' into Feature/program-manager/set-Complete-on-Done 2026-01-06 14:22:28 +03:30
a9789023ac feat: add HTTP POST endpoint for ChangePriority method in ProjectController 2026-01-06 13:46:45 +03:30
16b11a8bb8 feat: add ChangePriority method to ProjectController for task priority updates 2026-01-06 10:51:56 +03:30
SamSys
dd5455d80a ignore apsettings 2026-01-05 19:58:54 +03:30
9360dcad71 add UserSecretsId to ServiceHost.csproj 2026-01-05 19:14:11 +03:30
1971252713 feat: improve project board sorting by current user and task status 2026-01-05 17:52:12 +03:30
02cc099104 feat: update time calculation to include minutes in section time setting 2026-01-05 16:45:59 +03:30
582da511c6 feat: add progress percentage calculation to task sections 2026-01-01 19:25:28 +03:30
4ab9f60932 feat: add methods for creating, calculating installments, and removing loans in LoanController 2026-01-01 14:41:13 +03:30
9cfae54db3 feat: add LoanController for managing loan applications and details 2026-01-01 13:13:04 +03:30
49 changed files with 1040 additions and 419 deletions

2
.gitignore vendored
View File

@@ -362,6 +362,8 @@ MigrationBackup/
# # Fody - auto-generated XML schema # # Fody - auto-generated XML schema
# FodyWeavers.xsd # FodyWeavers.xsd
.idea .idea
/ServiceHost/appsettings.Development.json
/ServiceHost/appsettings.json
# Storage folder - ignore all uploaded files, thumbnails, and temporary files # Storage folder - ignore all uploaded files, thumbnails, and temporary files
ServiceHost/Storage ServiceHost/Storage

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Company.Domain.InstitutionContractSendFlagAgg;
/// <summary>
/// Interface برای Repository مربوط به فلگ ارسال قرارداد
/// </summary>
public interface IInstitutionContractSendFlagRepository
{
/// <summary>
/// ایجاد یک رکورد جدید برای فلگ ارسال قرارداد
/// </summary>
Task Create(InstitutionContractSendFlag flag);
/// <summary>
/// بازیابی فلگ بر اساس شناسه قرارداد
/// </summary>
Task<InstitutionContractSendFlag> GetByContractId(long contractId);
/// <summary>
/// به‌روزرسانی فلگ ارسال
/// </summary>
Task Update(InstitutionContractSendFlag flag);
/// <summary>
/// بررسی اینکه آیا قرارداد ارسال شده است
/// </summary>
Task<bool> IsContractSent(long contractId);
}

View File

@@ -0,0 +1,82 @@
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Company.Domain.InstitutionContractSendFlagAgg;
/// <summary>
/// نمایندگی فلگ ارسال قرارداد در MongoDB
/// این موجودیت برای ردیابی اینکه آیا قرارداد ارسال شده است استفاده می‌شود
/// </summary>
public class InstitutionContractSendFlag
{
public InstitutionContractSendFlag(long institutionContractId,bool isSent)
{
Id = Guid.NewGuid();
InstitutionContractId = institutionContractId;
IsSent = isSent;
CreatedDate = DateTime.Now;
}
/// <summary>
/// شناسه یکتای MongoDB
/// </summary>
[BsonId]
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; }
/// <summary>
/// شناسه قرارداد در SQL
/// </summary>
public long InstitutionContractId { get; set; }
/// <summary>
/// آیا قرارداد ارسال شده است
/// </summary>
public bool IsSent { get; set; }
/// <summary>
/// تاریخ و زمان ارسال
/// </summary>
public DateTime? SentDate { get; set; }
/// <summary>
/// تاریخ و زمان ایجاد رکورد
/// </summary>
public DateTime CreatedDate { get; set; }
/// <summary>
/// تاریخ و زمان آخرین به‌روزرسانی
/// </summary>
public DateTime? LastModifiedDate { get; set; }
/// <summary>
/// علامت‌گذاری قرارداد به عنوان ارسال‌شده
/// </summary>
public void MarkAsSent()
{
IsSent = true;
SentDate = DateTime.Now;
LastModifiedDate = DateTime.Now;
}
/// <summary>
/// بازگردانی علامت ارسال
/// </summary>
public void MarkAsNotSent()
{
IsSent = false;
SentDate = null;
LastModifiedDate = DateTime.Now;
}
/// <summary>
/// به‌روزرسانی علامت آخری اصلاح
/// </summary>
public void UpdateLastModified()
{
LastModifiedDate = DateTime.Now;
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Company.Domain.InstitutionContractSendFlagAgg;
using MongoDB.Driver;
namespace CompanyManagement.Infrastructure.Mongo.InstitutionContractSendFlagRepo;
/// <summary>
/// Repository برای مدیریت فلگ ارسال قرارداد در MongoDB
/// </summary>
public class InstitutionContractSendFlagRepository : IInstitutionContractSendFlagRepository
{
private readonly IMongoCollection<InstitutionContractSendFlag> _collection;
public InstitutionContractSendFlagRepository(IMongoDatabase database)
{
_collection = database.GetCollection<InstitutionContractSendFlag>("InstitutionContractSendFlag");
}
public async Task Create(InstitutionContractSendFlag flag)
{
await _collection.InsertOneAsync(flag);
}
public async Task<InstitutionContractSendFlag> GetByContractId(long contractId)
{
var filter = Builders<InstitutionContractSendFlag>.Filter
.Eq(x => x.InstitutionContractId, contractId);
return await _collection.Find(filter).FirstOrDefaultAsync();
}
public async Task Update(InstitutionContractSendFlag flag)
{
var filter = Builders<InstitutionContractSendFlag>.Filter
.Eq(x => x.InstitutionContractId, flag.InstitutionContractId);
await _collection.ReplaceOneAsync(filter, flag);
}
public async Task<bool> IsContractSent(long contractId)
{
var flag = await GetByContractId(contractId);
return flag != null && flag.IsSent;
}
public async Task Remove(long contractId)
{
var filter = Builders<InstitutionContractSendFlag>.Filter
.Eq(x => x.InstitutionContractId, contractId);
await _collection.DeleteOneAsync(filter);
}
}

View File

@@ -96,6 +96,8 @@ public class GetInstitutionContractListItemsViewModel
/// مبلغ قسط /// مبلغ قسط
/// </summary> /// </summary>
public double InstallmentAmount { get; set; } public double InstallmentAmount { get; set; }
public bool InstitutionContractIsSentFlag { get; set; }
} }
public class InstitutionContractListWorkshop public class InstitutionContractListWorkshop

View File

@@ -305,6 +305,14 @@ public interface IInstitutionContractApplication
Task<InstitutionContractDiscountResponse> SetDiscountForCreation(InstitutionContractSetDiscountForCreationRequest request); Task<InstitutionContractDiscountResponse> SetDiscountForCreation(InstitutionContractSetDiscountForCreationRequest request);
Task<InstitutionContractDiscountResponse> ResetDiscountForCreation(InstitutionContractResetDiscountForExtensionRequest request); Task<InstitutionContractDiscountResponse> ResetDiscountForCreation(InstitutionContractResetDiscountForExtensionRequest request);
Task<OperationResult> CreationComplete(InstitutionContractExtensionCompleteRequest request); Task<OperationResult> CreationComplete(InstitutionContractExtensionCompleteRequest request);
/// <summary>
/// تعیین فلگ ارسال قرارداد در MongoDB
/// اگر فلگ وجود نداشتن‌د ایجاد می‌کند
/// </summary>
/// <param name="request">درخواست تعیین فلگ</param>
/// <returns>نتیجه عملیات</returns>
Task<OperationResult> SetContractSendFlag(SetInstitutionContractSendFlagRequest request);
} }
public class CreationSetContractingPartyResponse public class CreationSetContractingPartyResponse

View File

@@ -0,0 +1,19 @@
namespace CompanyManagment.App.Contracts.InstitutionContract;
/// <summary>
/// درخواست برای تعیین فلگ ارسال قرارداد
/// </summary>
public class SetInstitutionContractSendFlagRequest
{
/// <summary>
/// شناسه قرارداد
/// </summary>
public long InstitutionContractId { get; set; }
/// <summary>
/// آیا قرارداد ارسال شده است
/// </summary>
public bool IsSent { get; set; }
}

View File

@@ -21,6 +21,6 @@ public class LoanGroupedViewModel
{ {
public List<LoanGroupedByDateViewModel> GroupedByDate { get; set; } public List<LoanGroupedByDateViewModel> GroupedByDate { get; set; }
public List<LoanGroupedByEmployeeViewModel>GroupedByEmployee { get; set; } public List<LoanGroupedByEmployeeViewModel>GroupedByEmployee { get; set; }
public List<LoanViewModel> LoanListViewModel { get; set; } public PagedResult<LoanViewModel> LoanListViewModel { get; set; }
} }

View File

@@ -16,5 +16,6 @@ public class LoanSearchViewModel
public string EndDate { get; set; } public string EndDate { get; set; }
public int PageIndex { get; set; } public int PageIndex { get; set; }
public int PageSize { get; set; } = 30;
public bool ShowAsGrouped { get; set; } public bool ShowAsGrouped { get; set; }
} }

View File

@@ -19,6 +19,7 @@ using Company.Domain.PaymentTransactionAgg;
using Company.Domain.RepresentativeAgg; using Company.Domain.RepresentativeAgg;
using Company.Domain.RollCallServiceAgg; using Company.Domain.RollCallServiceAgg;
using Company.Domain.WorkshopAgg; using Company.Domain.WorkshopAgg;
using Company.Domain.InstitutionContractSendFlagAgg;
using CompanyManagment.App.Contracts.FinancialInvoice; using CompanyManagment.App.Contracts.FinancialInvoice;
using CompanyManagment.App.Contracts.FinancialStatment; using CompanyManagment.App.Contracts.FinancialStatment;
using CompanyManagment.App.Contracts.InstitutionContract; using CompanyManagment.App.Contracts.InstitutionContract;
@@ -51,6 +52,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
private readonly IPaymentTransactionRepository _paymentTransactionRepository; private readonly IPaymentTransactionRepository _paymentTransactionRepository;
private readonly IRollCallServiceRepository _rollCallServiceRepository; private readonly IRollCallServiceRepository _rollCallServiceRepository;
private readonly ISepehrPaymentGatewayService _sepehrPaymentGatewayService; private readonly ISepehrPaymentGatewayService _sepehrPaymentGatewayService;
private readonly IInstitutionContractSendFlagRepository _institutionContractSendFlagRepository;
public InstitutionContractApplication(IInstitutionContractRepository institutionContractRepository, public InstitutionContractApplication(IInstitutionContractRepository institutionContractRepository,
@@ -62,7 +64,8 @@ public class InstitutionContractApplication : IInstitutionContractApplication
IAccountApplication accountApplication, ISmsService smsService, IAccountApplication accountApplication, ISmsService smsService,
IFinancialInvoiceRepository financialInvoiceRepository, IHttpClientFactory httpClientFactory, IFinancialInvoiceRepository financialInvoiceRepository, IHttpClientFactory httpClientFactory,
IPaymentTransactionRepository paymentTransactionRepository, IRollCallServiceRepository rollCallServiceRepository, IPaymentTransactionRepository paymentTransactionRepository, IRollCallServiceRepository rollCallServiceRepository,
ISepehrPaymentGatewayService sepehrPaymentGatewayService,ILogger<SepehrPaymentGateway> sepehrGatewayLogger) ISepehrPaymentGatewayService sepehrPaymentGatewayService,ILogger<SepehrPaymentGateway> sepehrGatewayLogger,
IInstitutionContractSendFlagRepository institutionContractSendFlagRepository)
{ {
_institutionContractRepository = institutionContractRepository; _institutionContractRepository = institutionContractRepository;
_contractingPartyRepository = contractingPartyRepository; _contractingPartyRepository = contractingPartyRepository;
@@ -80,6 +83,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
_rollCallServiceRepository = rollCallServiceRepository; _rollCallServiceRepository = rollCallServiceRepository;
_sepehrPaymentGatewayService = sepehrPaymentGatewayService; _sepehrPaymentGatewayService = sepehrPaymentGatewayService;
_paymentGateway = new SepehrPaymentGateway(httpClientFactory,sepehrGatewayLogger); _paymentGateway = new SepehrPaymentGateway(httpClientFactory,sepehrGatewayLogger);
_institutionContractSendFlagRepository = institutionContractSendFlagRepository;
} }
public OperationResult Create(CreateInstitutionContract command) public OperationResult Create(CreateInstitutionContract command)
@@ -894,6 +898,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
return opration.Succcedded(); return opration.Succcedded();
} }
public void CreateContractingPartyAccount(long contractingPartyid, long accountId) public void CreateContractingPartyAccount(long contractingPartyid, long accountId)
{ {
_institutionContractRepository.CreateContractingPartyAccount(contractingPartyid, accountId); _institutionContractRepository.CreateContractingPartyAccount(contractingPartyid, accountId);
@@ -1820,7 +1825,60 @@ public class InstitutionContractApplication : IInstitutionContractApplication
installments.Add(lastInstallment); installments.Add(lastInstallment);
return installments; return installments;
} }
} }
/// <summary>
/// تعیین فلگ ارسال قرارداد
/// اگر فلگ وجود نداشتن‌د ایجاد می‌کند
/// </summary>
public async Task<OperationResult> SetContractSendFlag(SetInstitutionContractSendFlagRequest request)
{
var operationResult = new OperationResult();
try
{
// بازیابی قرارداد از SQL
var contract = _institutionContractRepository.Get(request.InstitutionContractId);
if (contract == null)
return operationResult.Failed("قرارداد مورد نظر یافت نشد");
// بررسی اینکه آیا فلگ در MongoDB وجود دارد
var existingFlag = await _institutionContractSendFlagRepository
.GetByContractId(request.InstitutionContractId);
if (existingFlag != null)
{
// اگر فلگ وجود داشتن‌د، آن را اپدیت کنیم
if (request.IsSent)
{
existingFlag.MarkAsSent();
}
else
{
existingFlag.MarkAsNotSent();
}
existingFlag.UpdateLastModified();
await _institutionContractSendFlagRepository.Update(existingFlag);
}
else
{
// اگر فلگ وجود ندارد، آن را ایجاد کنیم
var newFlag = new InstitutionContractSendFlag(
request.InstitutionContractId,
request.IsSent
);
await _institutionContractSendFlagRepository.Create(newFlag);
}
return operationResult.Succcedded();
}
catch (Exception ex)
{
return operationResult.Failed($"خطا در تعیین فلگ ارسال: {ex.Message}");
}
}
} }
#region CustomViewModels #region CustomViewModels

View File

@@ -11,6 +11,7 @@ using Company.Domain.InstitutionContractAgg;
using Company.Domain.InstitutionContractAmendmentTempAgg; using Company.Domain.InstitutionContractAmendmentTempAgg;
using Company.Domain.InstitutionContractContactInfoAgg; using Company.Domain.InstitutionContractContactInfoAgg;
using Company.Domain.InstitutionContractExtensionTempAgg; using Company.Domain.InstitutionContractExtensionTempAgg;
using Company.Domain.InstitutionContractSendFlagAgg;
using Company.Domain.InstitutionPlanAgg; using Company.Domain.InstitutionPlanAgg;
using Company.Domain.SmsResultAgg; using Company.Domain.SmsResultAgg;
using Company.Domain.WorkshopAgg; using Company.Domain.WorkshopAgg;
@@ -42,6 +43,7 @@ using AccountManagement.Application.Contracts.Account;
using Company.Domain.InstitutionContractCreationTempAgg; using Company.Domain.InstitutionContractCreationTempAgg;
using Company.Domain.RepresentativeAgg; using Company.Domain.RepresentativeAgg;
using Company.Domain.TemporaryClientRegistrationAgg; using Company.Domain.TemporaryClientRegistrationAgg;
using Company.Domain.InstitutionContractSendFlagAgg;
using ContractingPartyAccount = Company.Domain.ContractingPartyAccountAgg.ContractingPartyAccount; using ContractingPartyAccount = Company.Domain.ContractingPartyAccountAgg.ContractingPartyAccount;
using FinancialStatment = Company.Domain.FinancialStatmentAgg.FinancialStatment; using FinancialStatment = Company.Domain.FinancialStatmentAgg.FinancialStatment;
using String = System.String; using String = System.String;
@@ -57,6 +59,7 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
private readonly IMongoCollection<InstitutionContractExtensionTemp> _institutionExtensionTemp; private readonly IMongoCollection<InstitutionContractExtensionTemp> _institutionExtensionTemp;
private readonly IMongoCollection<InstitutionContractAmendmentTemp> _institutionAmendmentTemp; private readonly IMongoCollection<InstitutionContractAmendmentTemp> _institutionAmendmentTemp;
private readonly IMongoCollection<InstitutionContractCreationTemp> _institutionContractCreationTemp; private readonly IMongoCollection<InstitutionContractCreationTemp> _institutionContractCreationTemp;
private readonly IMongoCollection<InstitutionContractSendFlag> _institutionContractSendFlag;
private readonly IPlanPercentageRepository _planPercentageRepository; private readonly IPlanPercentageRepository _planPercentageRepository;
private readonly ISmsService _smsService; private readonly ISmsService _smsService;
private readonly ISmsResultRepository _smsResultRepository; private readonly ISmsResultRepository _smsResultRepository;
@@ -114,6 +117,8 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
database.GetCollection<InstitutionContractAmendmentTemp>("InstitutionContractAmendmentTemp"); database.GetCollection<InstitutionContractAmendmentTemp>("InstitutionContractAmendmentTemp");
_institutionContractCreationTemp = _institutionContractCreationTemp =
database.GetCollection<InstitutionContractCreationTemp>("InstitutionContractCreationTemp"); database.GetCollection<InstitutionContractCreationTemp>("InstitutionContractCreationTemp");
_institutionContractSendFlag =
database.GetCollection<InstitutionContractSendFlag>("InstitutionContractSendFlag");
} }
public EditInstitutionContract GetDetails(long id) public EditInstitutionContract GetDetails(long id)
@@ -1353,6 +1358,12 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
.Where(x => contractIds.Contains(x.InstitutionContractId)) .Where(x => contractIds.Contains(x.InstitutionContractId))
.ToDictionaryAsync(x => x.InstitutionContractId, x => x); .ToDictionaryAsync(x => x.InstitutionContractId, x => x);
// بارگذاری وضعیت ارسال قراردادها از MongoDB - کوئری مستقیم
var filter = Builders<InstitutionContractSendFlag>.Filter
.In(x => x.InstitutionContractId, contractIds);
var sendFlagsList = await _institutionContractSendFlag.Find(filter).ToListAsync();
var sendFlags = sendFlagsList.ToDictionary(x => x.InstitutionContractId, x => x.IsSent);
var financialStatements = _context.FinancialStatments.Include(x => x.FinancialTransactionList) var financialStatements = _context.FinancialStatments.Include(x => x.FinancialTransactionList)
.Where(x => contractingPartyIds.Contains(x.ContractingPartyId)).ToList(); .Where(x => contractingPartyIds.Contains(x.ContractingPartyId)).ToList();
var res = new PagedResult<GetInstitutionContractListItemsViewModel>() var res = new PagedResult<GetInstitutionContractListItemsViewModel>()
@@ -1462,7 +1473,8 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
Workshops = workshopDetails, Workshops = workshopDetails,
IsInPersonContract = workshopGroup?.CurrentWorkshops IsInPersonContract = workshopGroup?.CurrentWorkshops
.Any(y => y.Services.ContractInPerson) ?? true, .Any(y => y.Services.ContractInPerson) ?? true,
IsOldContract = x.contract.SigningType == InstitutionContractSigningType.Legacy IsOldContract = x.contract.SigningType == InstitutionContractSigningType.Legacy,
InstitutionContractIsSentFlag = sendFlags.ContainsKey(x.contract.id) ? sendFlags[x.contract.id] : false
}; };
}).ToList() }).ToList()
}; };
@@ -6334,10 +6346,10 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
} }
/// <summary> /// <summary>
///دریافت لیست پیامک قرادا های آبی بدهکار ///دریافت لیست پیامک قراداد های آبی بدهکار
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
//public async Task<List<SmsListData>> GetBlueSmsListData() //public async Task<List<SmsListData>> GetWarningSmsListData()
//{ //{
// var institutionContracts = await _context.InstitutionContractSet.AsQueryable().Select(x => new InstitutionContractViewModel // var institutionContracts = await _context.InstitutionContractSet.AsQueryable().Select(x => new InstitutionContractViewModel
@@ -6358,6 +6370,8 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
// }).Where(x => x.IsActiveString == "blue" && // }).Where(x => x.IsActiveString == "blue" &&
// x.ContractAmountDouble > 0).GroupBy(x => x.ContractingPartyId).Select(x => x.First()).ToListAsync(); // x.ContractAmountDouble > 0).GroupBy(x => x.ContractingPartyId).Select(x => x.First()).ToListAsync();
// var institutionContractsIds = institutionContracts.Select(x => x.id).ToList();
// // قرارداد هایی که بطور یکجا پرداخت شده اند // // قرارداد هایی که بطور یکجا پرداخت شده اند
// var paidInFull = institutionContracts.Where(x => // var paidInFull = institutionContracts.Where(x =>
// x.SigningType != InstitutionContractSigningType.Legacy && x.IsInstallment == false && x.SigningType != null).ToList(); // x.SigningType != InstitutionContractSigningType.Legacy && x.IsInstallment == false && x.SigningType != null).ToList();
@@ -6376,7 +6390,7 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
// .Where(x => institutionContracts.Select(ins => ins.Id).Contains(x.InstitutionContractId)) // .Where(x => institutionContracts.Select(ins => ins.Id).Contains(x.InstitutionContractId))
// .Where(x => x.SendSms && x.PhoneType == "شماره همراه" && !string.IsNullOrWhiteSpace(x.PhoneNumber) && // .Where(x => x.SendSms && x.PhoneType == "شماره همراه" && !string.IsNullOrWhiteSpace(x.PhoneNumber) &&
// x.PhoneNumber.Length == 11).ToListAsync(); // x.PhoneNumber.Length == 11).ToListAsync();
// var legalActionSentSms =await _context.SmsResults // var legalActionSentSms = await _context.SmsResults
// .Where(x => x.TypeOfSms == "اقدام قضایی").ToListAsync(); // .Where(x => x.TypeOfSms == "اقدام قضایی").ToListAsync();
// var warningSentSms = await _context.SmsResults.Where(x => x.TypeOfSms.Contains("هشدار")).ToListAsync(); // var warningSentSms = await _context.SmsResults.Where(x => x.TypeOfSms.Contains("هشدار")).ToListAsync();
@@ -6389,8 +6403,13 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
// { // {
// var contractingParty = GetDetails(item.ContractingPartyId); // var contractingParty = GetDetails(item.ContractingPartyId);
// bool hasLegalActionSentSms = legalActionSentSms.Any(x => x.InstitutionContractId == item.Id); // bool hasLegalActionSentSms = legalActionSentSms.Any(x => x.InstitutionContractId == item.Id);
// int year = Convert.ToInt32(item.ContractEndFa.Substring(0, 4));
// int month = Convert.ToInt32(item.ContractEndFa.Substring(5, 2));
// var endOfContractNextMonthStart = new PersianDateTime(year, month, 1).AddMonths(1);
// var endOfContractNextMonthEnd = (($"{endOfContractNextMonthStart}").FindeEndOfMonth()).ToGeorgianDateTime();
// var now = DateTime.Now
// if (!string.IsNullOrWhiteSpace(contractingParty.LName) && !hasLegalActionSentSms) // if (!string.IsNullOrWhiteSpace(contractingParty.LName) && !hasLegalActionSentSms && now.Date <= endOfContractNextMonthEnd.Date)
// { // {
// //Thread.Sleep(500); // //Thread.Sleep(500);
// var partyName = contractingParty.LName; // var partyName = contractingParty.LName;
@@ -6446,7 +6465,7 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
// var isLastAlarmSend = _context.SmsResults.Any(x => ( // var isLastAlarmSend = _context.SmsResults.Any(x => (
// x.ContractingPatyId == contractingParty.Id && // x.ContractingPatyId == contractingParty.Id &&
// x.Mobile == number.PhoneNumber) && (x.TypeOfSms == "اقدام قضایی" || x.TypeOfSms == "هشدار دوم")); // x.Mobile == number.PhoneNumber) && (x.TypeOfSms == "اقدام قضایی" || x.TypeOfSms == "هشدار دوم"));
// var t = warningSentSms.Any(x=> x.)
// if (!string.IsNullOrWhiteSpace(number.PhoneNumber) && // if (!string.IsNullOrWhiteSpace(number.PhoneNumber) &&
// number.PhoneNumber.Length == 11 && !isSend && !isLastAlarmSend) // number.PhoneNumber.Length == 11 && !isSend && !isLastAlarmSend)
// { // {

View File

@@ -171,22 +171,29 @@ public class LoanRepository : RepositoryBase<long, Loan>, ILoanRepository
query = query.Where(x => x.StartInstallmentPayment >= startDate && x.StartInstallmentPayment <= endDate); query = query.Where(x => x.StartInstallmentPayment >= startDate && x.StartInstallmentPayment <= endDate);
} }
result.LoanListViewModel = query.OrderByDescending(x => x.StartInstallmentPayment).Skip(searchModel.PageIndex) result.LoanListViewModel = new PagedResult<LoanViewModel>()
.Take(30).ToList() {
.Select(x => new LoanViewModel() TotalCount = query.Count(),
{ List = query.OrderByDescending(x => x.StartInstallmentPayment)
EmployeeFullName = employees.FirstOrDefault(e => e.id == x.EmployeeId).FullName, .ApplyPagination(searchModel.PageIndex, searchModel.PageSize)
PersonnelCode = personnelCodes.FirstOrDefault(p => p.EmployeeId == x.EmployeeId).PersonnelCode.ToString(), .Take(30).ToList()
Amount = x.Amount.ToMoney(), .Select(x => new LoanViewModel()
AmountPerMonth = x.AmountPerMonth.ToMoney(), {
StartDateTime = x.StartInstallmentPayment.ToFarsi(), EmployeeFullName = employees.FirstOrDefault(e => e.id == x.EmployeeId).FullName,
Count = x.Count, PersonnelCode = personnelCodes.FirstOrDefault(p => p.EmployeeId == x.EmployeeId).PersonnelCode
Id = x.id, .ToString(),
WorkshopId = x.WorkshopId, Amount = x.Amount.ToMoney(),
EmployeeId = x.EmployeeId, AmountPerMonth = x.AmountPerMonth.ToMoney(),
YearFa = pc.GetYear(x.StartInstallmentPayment).ToString(), StartDateTime = x.StartInstallmentPayment.ToFarsi(),
Count = x.Count,
Id = x.id,
WorkshopId = x.WorkshopId,
EmployeeId = x.EmployeeId,
YearFa = pc.GetYear(x.StartInstallmentPayment).ToString(),
}).ToList()
};
}).ToList();
return result; return result;
} }

View File

@@ -61,6 +61,7 @@ using Company.Domain.HolidayItemAgg;
using Company.Domain.InstitutionContractAgg; using Company.Domain.InstitutionContractAgg;
using Company.Domain.InstitutionContractContactInfoAgg; using Company.Domain.InstitutionContractContactInfoAgg;
using Company.Domain.InstitutionContractExtensionTempAgg; using Company.Domain.InstitutionContractExtensionTempAgg;
using Company.Domain.InstitutionContractSendFlagAgg;
using Company.Domain.InstitutionPlanAgg; using Company.Domain.InstitutionPlanAgg;
using Company.Domain.InsuranceAgg; using Company.Domain.InsuranceAgg;
using Company.Domain.InsuranceEmployeeInfoAgg; using Company.Domain.InsuranceEmployeeInfoAgg;
@@ -123,6 +124,7 @@ using Company.Domain.ZoneAgg;
using CompanyManagement.Infrastructure.Excel.SalaryAid; using CompanyManagement.Infrastructure.Excel.SalaryAid;
using CompanyManagement.Infrastructure.Mongo.EmployeeFaceEmbeddingRepo; using CompanyManagement.Infrastructure.Mongo.EmployeeFaceEmbeddingRepo;
using CompanyManagement.Infrastructure.Mongo.InstitutionContractInsertTempRepo; using CompanyManagement.Infrastructure.Mongo.InstitutionContractInsertTempRepo;
using CompanyManagement.Infrastructure.Mongo.InstitutionContractSendFlagRepo;
using CompanyManagment.App.Contracts.AdminMonthlyOverview; using CompanyManagment.App.Contracts.AdminMonthlyOverview;
using CompanyManagment.App.Contracts.AndroidApkVersion; using CompanyManagment.App.Contracts.AndroidApkVersion;
using CompanyManagment.App.Contracts.AuthorizedPerson; using CompanyManagment.App.Contracts.AuthorizedPerson;
@@ -658,6 +660,9 @@ public class PersonalBootstrapper
services.AddTransient<ICameraBugReportApplication, CameraBugReportApplication>(); services.AddTransient<ICameraBugReportApplication, CameraBugReportApplication>();
services.AddTransient<ICameraBugReportRepository, CameraBugReportRepository>(); // MongoDB Implementation services.AddTransient<ICameraBugReportRepository, CameraBugReportRepository>(); // MongoDB Implementation
// InstitutionContractSendFlag - MongoDB
services.AddTransient<IInstitutionContractSendFlagRepository, InstitutionContractSendFlagRepository>();
services.AddDbContext<CompanyContext>(x => x.UseSqlServer(connectionString)); services.AddDbContext<CompanyContext>(x => x.UseSqlServer(connectionString));
} }
} }

View File

@@ -10,7 +10,7 @@ public record AddTaskToPhaseCommand(
Guid PhaseId, Guid PhaseId,
string Name, string Name,
string? Description = null, string? Description = null,
TaskPriority Priority = TaskPriority.Medium, ProjectTaskPriority Priority = ProjectTaskPriority.Medium,
int OrderIndex = 0, int OrderIndex = 0,
DateTime? DueDate = null DateTime? DueDate = null
) : IBaseCommand; ) : IBaseCommand;

View File

@@ -0,0 +1,62 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoPendingFullTimeTaskSections;
public record AutoPendingFullTimeTaskSectionsCommand : IBaseCommand;
public class AutoPendingFullTimeTaskSectionsCommandHandler : IBaseCommandHandler<AutoPendingFullTimeTaskSectionsCommand>
{
private readonly ITaskSectionRepository _taskSectionRepository;
private readonly IUnitOfWork _unitOfWork;
public AutoPendingFullTimeTaskSectionsCommandHandler(
ITaskSectionRepository taskSectionRepository,
IUnitOfWork unitOfWork)
{
_taskSectionRepository = taskSectionRepository;
_unitOfWork = unitOfWork;
}
public async Task<OperationResult> Handle(AutoPendingFullTimeTaskSectionsCommand request, CancellationToken cancellationToken)
{
try
{
// تمام سکشن‌هایی که هنوز Pending یا Completed نشده‌اند را دریافت کن
var taskSections = await _taskSectionRepository.GetAllNotCompletedOrPendingIncludeAllAsync(cancellationToken);
foreach (var section in taskSections)
{
var totalSpent = section.GetTotalTimeSpent();
var estimate = section.FinalEstimatedHours;
if (estimate.TotalMinutes <= 0)
continue; // تسک بدون تخمین را نادیده بگیر
if (totalSpent >= estimate)
{
// مهم: وضعیت را مستقل از فعال/غیرفعال بودن فعالیت‌ها PendingForCompletion کنیم
if (section.IsInProgress())
{
// اگر فعالیت فعال دارد، با وضعیت جدید متوقف شود
section.StopWork(TaskSectionStatus.PendingForCompletion, "اتمام خودکار - رسیدن به ۱۰۰٪ زمان تخمینی");
}
else
{
section.UpdateStatus(TaskSectionStatus.PendingForCompletion);
}
}
}
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
catch (Exception ex)
{
return OperationResult.Failure($"خطا در در انتظار تکمیل قرار دادن خودکار تسک‌ها: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,109 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeTaskPriority;
public record ChangeTaskPriorityCommand(
Guid Id,
ProjectHierarchyLevel Level,
ProjectTaskPriority Priority
) : IBaseCommand;
public class ChangeTaskPriorityCommandHandler : IBaseCommandHandler<ChangeTaskPriorityCommand>
{
private readonly IProjectTaskRepository _taskRepository;
private readonly IProjectPhaseRepository _phaseRepository;
private readonly IProjectRepository _projectRepository;
private readonly IUnitOfWork _unitOfWork;
public ChangeTaskPriorityCommandHandler(
IProjectTaskRepository taskRepository,
IProjectPhaseRepository phaseRepository,
IProjectRepository projectRepository,
IUnitOfWork unitOfWork)
{
_taskRepository = taskRepository;
_phaseRepository = phaseRepository;
_projectRepository = projectRepository;
_unitOfWork = unitOfWork;
}
public async Task<OperationResult> Handle(ChangeTaskPriorityCommand request, CancellationToken cancellationToken)
{
switch (request.Level)
{
case ProjectHierarchyLevel.Task:
return await HandleTaskLevelAsync(request.Id, request.Priority, cancellationToken);
case ProjectHierarchyLevel.Phase:
return await HandlePhaseLevelAsync(request.Id, request.Priority, cancellationToken);
case ProjectHierarchyLevel.Project:
return await HandleProjectLevelAsync(request.Id, request.Priority, cancellationToken);
default:
return OperationResult.Failure("سطح نامعتبر است");
}
}
// Task-level priority update
private async Task<OperationResult> HandleTaskLevelAsync(Guid taskId, ProjectTaskPriority priority, CancellationToken ct)
{
var task = await _taskRepository.GetByIdAsync(taskId, ct);
if (task is null)
return OperationResult.NotFound("تسک یافت نشد");
if (task.Priority != priority)
{
task.SetPriority(priority);
}
await _unitOfWork.SaveChangesAsync(ct);
return OperationResult.Success();
}
// Phase-level bulk priority update
private async Task<OperationResult> HandlePhaseLevelAsync(Guid phaseId, ProjectTaskPriority priority, CancellationToken ct)
{
var phase = await _phaseRepository.GetWithTasksAsync(phaseId);
if (phase is null)
return OperationResult.NotFound("فاز یافت نشد");
var tasks = phase.Tasks?.ToList() ?? new List<Domain.ProjectAgg.Entities.ProjectTask>();
foreach (var t in tasks)
{
if (t.Priority != priority)
{
t.SetPriority(priority);
}
}
await _unitOfWork.SaveChangesAsync(ct);
return OperationResult.Success();
}
// Project-level bulk priority update across all phases
private async Task<OperationResult> HandleProjectLevelAsync(Guid projectId, ProjectTaskPriority priority, CancellationToken ct)
{
var project = await _projectRepository.GetWithFullHierarchyAsync(projectId);
if (project is null)
return OperationResult.NotFound("پروژه یافت نشد");
var phases = project.Phases?.ToList() ?? new List<Domain.ProjectAgg.Entities.ProjectPhase>();
foreach (var phase in phases)
{
var tasks = phase.Tasks?.ToList() ?? new List<Domain.ProjectAgg.Entities.ProjectTask>();
foreach (var t in tasks)
{
if (t.Priority != priority)
{
t.SetPriority(priority);
}
}
}
await _unitOfWork.SaveChangesAsync(ct);
return OperationResult.Success();
}
}

View File

@@ -352,7 +352,8 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
private void SetSectionTime(TaskSection section, SetTimeProjectSkillItem sectionItem, long? addedByUserId) private void SetSectionTime(TaskSection section, SetTimeProjectSkillItem sectionItem, long? addedByUserId)
{ {
var initData = sectionItem.InitData; var initData = sectionItem.InitData;
var initialTime = TimeSpan.FromHours(initData.Hours); var initialTime = TimeSpan.FromHours(initData.Hours)
.Add(TimeSpan.FromMinutes(initData.Minutes));
if (initialTime <= TimeSpan.Zero) if (initialTime <= TimeSpan.Zero)
{ {
@@ -364,10 +365,26 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
section.ClearAdditionalTimes(); section.ClearAdditionalTimes();
// افزودن زمان‌های اضافی // افزودن زمان‌های اضافی
bool hasAdditionalTime = false;
foreach (var additionalTime in sectionItem.AdditionalTime) foreach (var additionalTime in sectionItem.AdditionalTime)
{ {
var additionalTimeSpan = TimeSpan.FromHours(additionalTime.Hours).Add(TimeSpan.FromMinutes(additionalTime.Minutes)); var additionalTimeSpan = TimeSpan.FromHours(additionalTime.Hours).Add(TimeSpan.FromMinutes(additionalTime.Minutes));
section.AddAdditionalTime(additionalTimeSpan, additionalTime.Description, addedByUserId); section.AddAdditionalTime(additionalTimeSpan, additionalTime.Description, addedByUserId);
hasAdditionalTime = true;
}
// تغییر status به Incomplete فقط اگر زمان اضافی اضافه شده باشد و در وضعیتی غیر از ReadyToStart باشد
if (hasAdditionalTime && section.Status != TaskSectionStatus.ReadyToStart)
{
// اگر سکشن درحال انجام است، باید متوقف شود قبل از تغییر status
if (section.Status == TaskSectionStatus.InProgress)
{
section.StopWork(TaskSectionStatus.Incomplete);
}
else
{
section.UpdateStatus(TaskSectionStatus.Incomplete);
}
} }
} }

View File

@@ -89,6 +89,7 @@ public class ProjectSectionDto
public TimeSpan FinalEstimatedHours { get; set; } public TimeSpan FinalEstimatedHours { get; set; }
public TimeSpan TotalTimeSpent { get; set; } public TimeSpan TotalTimeSpent { get; set; }
public double ProgressPercentage { get; set; }
public bool IsCompleted { get; set; } public bool IsCompleted { get; set; }
public bool IsInProgress { get; set; } public bool IsInProgress { get; set; }

View File

@@ -166,6 +166,7 @@ public static class ProjectMappingExtensions
CreationDate = section.CreationDate, CreationDate = section.CreationDate,
FinalEstimatedHours = section.FinalEstimatedHours, FinalEstimatedHours = section.FinalEstimatedHours,
TotalTimeSpent = section.GetTotalTimeSpent(), TotalTimeSpent = section.GetTotalTimeSpent(),
ProgressPercentage = section.GetProgressPercentage(),
IsCompleted = section.IsCompleted(), IsCompleted = section.IsCompleted(),
IsInProgress = section.IsInProgress(), IsInProgress = section.IsInProgress(),
Activities = section.Activities.Select(a => a.ToDto()).ToList(), Activities = section.Activities.Select(a => a.ToDto()).ToList(),
@@ -188,6 +189,7 @@ public static class ProjectMappingExtensions
CreationDate = section.CreationDate, CreationDate = section.CreationDate,
FinalEstimatedHours = section.FinalEstimatedHours, FinalEstimatedHours = section.FinalEstimatedHours,
TotalTimeSpent = section.GetTotalTimeSpent(), TotalTimeSpent = section.GetTotalTimeSpent(),
ProgressPercentage = section.GetProgressPercentage(),
IsCompleted = section.IsCompleted(), IsCompleted = section.IsCompleted(),
IsInProgress = section.IsInProgress() IsInProgress = section.IsInProgress()
// No activities or additional times for summary // No activities or additional times for summary

View File

@@ -1,20 +1,28 @@
using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList; namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
public record GetProjectListDto
// Base DTO shared across project, phase, and task
public class GetProjectItemDto
{ {
public Guid Id { get; init; } public Guid Id { get; init; }
public string Name { get; init; } = string.Empty; public string Name { get; init; } = string.Empty;
public int Percentage { get; init; } public int Percentage { get; init; }
public ProjectHierarchyLevel Level { get; init; } public ProjectHierarchyLevel Level { get; init; }
public Guid? ParentId { get; init; } public Guid? ParentId { get; init; }
public bool HasFront { get; set; }
public bool HasBackend { get; set; }
public bool HasDesign { get; set; }
public int TotalHours { get; set; } public int TotalHours { get; set; }
public int Minutes { get; set; } public int Minutes { get; set; }
public AssignmentStatus Front { get; set; }
public AssignmentStatus Backend { get; set; }
public AssignmentStatus Design { get; set; }
} }
// Project DTO (no extra fields; inherits from base)
public class GetProjectDto : GetProjectItemDto
{
}
// Phase DTO (no extra fields; inherits from base)
public class GetPhaseDto : GetProjectItemDto
{
}

View File

@@ -3,6 +3,7 @@ using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList; namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
@@ -17,47 +18,47 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
public async Task<OperationResult<GetProjectsListResponse>> Handle(GetProjectsListQuery request, CancellationToken cancellationToken) public async Task<OperationResult<GetProjectsListResponse>> Handle(GetProjectsListQuery request, CancellationToken cancellationToken)
{ {
List<GetProjectListDto> projects; var projects = new List<GetProjectDto>();
var phases = new List<GetPhaseDto>();
var tasks = new List<GetTaskDto>();
switch (request.HierarchyLevel) switch (request.HierarchyLevel)
{ {
case ProjectHierarchyLevel.Project: case ProjectHierarchyLevel.Project:
projects = await GetProjects(request.ParentId, cancellationToken); projects = await GetProjects(request.ParentId, cancellationToken);
await SetSkillFlags(projects, cancellationToken);
break; break;
case ProjectHierarchyLevel.Phase: case ProjectHierarchyLevel.Phase:
projects = await GetPhases(request.ParentId, cancellationToken); phases = await GetPhases(request.ParentId, cancellationToken);
await SetSkillFlags(phases, cancellationToken);
break; break;
case ProjectHierarchyLevel.Task: case ProjectHierarchyLevel.Task:
projects = await GetTasks(request.ParentId, cancellationToken); tasks = await GetTasks(request.ParentId, cancellationToken);
// Tasks don't need SetSkillFlags because they have Sections list
break; break;
default: default:
return OperationResult<GetProjectsListResponse>.Failure("سطح سلسله مراتب نامعتبر است"); return OperationResult<GetProjectsListResponse>.Failure("سطح سلسله مراتب نامعتبر است");
} }
await SetSkillFlags(projects, cancellationToken);
var response = new GetProjectsListResponse(projects); var response = new GetProjectsListResponse(projects, phases, tasks);
return OperationResult<GetProjectsListResponse>.Success(response); return OperationResult<GetProjectsListResponse>.Success(response);
} }
private async Task<List<GetProjectListDto>> GetProjects(Guid? parentId, CancellationToken cancellationToken) private async Task<List<GetProjectDto>> GetProjects(Guid? parentId, CancellationToken cancellationToken)
{ {
var query = _context.Projects.AsQueryable(); var query = _context.Projects.AsQueryable();
// پروژه‌ها سطح بالا هستند و parentId ندارند، فقط در صورت null بودن parentId نمایش داده می‌شوند
if (parentId.HasValue) if (parentId.HasValue)
{ {
return new List<GetProjectListDto>(); // پروژه‌ها parent ندارند return new List<GetProjectDto>();
} }
var entities = await query
var projects = await query
.OrderByDescending(p => p.CreationDate) .OrderByDescending(p => p.CreationDate)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var result = new List<GetProjectListDto>(); var result = new List<GetProjectDto>();
foreach (var project in entities)
foreach (var project in projects)
{ {
var (percentage, totalTime) = await CalculateProjectPercentage(project, cancellationToken); var (percentage, totalTime) = await CalculateProjectPercentage(project, cancellationToken);
result.Add(new GetProjectListDto result.Add(new GetProjectDto
{ {
Id = project.Id, Id = project.Id,
Name = project.Name, Name = project.Name,
@@ -68,28 +69,24 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
Minutes = totalTime.Minutes, Minutes = totalTime.Minutes,
}); });
} }
return result; return result;
} }
private async Task<List<GetProjectListDto>> GetPhases(Guid? parentId, CancellationToken cancellationToken) private async Task<List<GetPhaseDto>> GetPhases(Guid? parentId, CancellationToken cancellationToken)
{ {
var query = _context.ProjectPhases.AsQueryable(); var query = _context.ProjectPhases.AsQueryable();
if (parentId.HasValue) if (parentId.HasValue)
{ {
query = query.Where(x => x.ProjectId == parentId); query = query.Where(x => x.ProjectId == parentId);
} }
var entities = await query
var phases = await query
.OrderByDescending(p => p.CreationDate) .OrderByDescending(p => p.CreationDate)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var result = new List<GetProjectListDto>(); var result = new List<GetPhaseDto>();
foreach (var phase in entities)
foreach (var phase in phases)
{ {
var (percentage, totalTime) = await CalculatePhasePercentage(phase, cancellationToken); var (percentage, totalTime) = await CalculatePhasePercentage(phase, cancellationToken);
result.Add(new GetProjectListDto result.Add(new GetPhaseDto
{ {
Id = phase.Id, Id = phase.Id,
Name = phase.Name, Name = phase.Name,
@@ -100,28 +97,87 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
Minutes = totalTime.Minutes, Minutes = totalTime.Minutes,
}); });
} }
return result; return result;
} }
private async Task<List<GetProjectListDto>> GetTasks(Guid? parentId, CancellationToken cancellationToken) private async Task<List<GetTaskDto>> GetTasks(Guid? parentId, CancellationToken cancellationToken)
{ {
var query = _context.ProjectTasks.AsQueryable(); var query = _context.ProjectTasks.AsQueryable();
if (parentId.HasValue) if (parentId.HasValue)
{ {
query = query.Where(x => x.PhaseId == parentId); query = query.Where(x => x.PhaseId == parentId);
} }
var entities = await query
var tasks = await query
.OrderByDescending(t => t.CreationDate) .OrderByDescending(t => t.CreationDate)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var result = new List<GetProjectListDto>(); var result = new List<GetTaskDto>();
// دریافت تمام Skills
var allSkills = await _context.Skills
.Select(s => new { s.Id, s.Name })
.ToListAsync(cancellationToken);
foreach (var task in tasks) foreach (var task in entities)
{ {
var (percentage, totalTime) = await CalculateTaskPercentage(task, cancellationToken); var (percentage, totalTime) = await CalculateTaskPercentage(task, cancellationToken);
result.Add(new GetProjectListDto var sections = await _context.TaskSections
.Include(s => s.Activities)
.Include(s => s.Skill)
.Where(s => s.TaskId == task.Id)
.ToListAsync(cancellationToken);
// جمع‌آوری تمام userId های مورد نیاز
var userIds = sections
.Where(s => s.CurrentAssignedUserId > 0)
.Select(s => s.CurrentAssignedUserId)
.Distinct()
.ToList();
// دریافت اطلاعات کاربران
var users = await _context.Users
.Where(u => userIds.Contains(u.Id))
.Select(u => new { u.Id, u.FullName })
.ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
// محاسبه SpentTime و RemainingTime
var spentTime = TimeSpan.FromTicks(sections.Sum(s => s.Activities.Sum(a => a.GetTimeSpent().Ticks)));
var remainingTime = totalTime - spentTime;
// ساخت section DTOs برای تمام Skills
var sectionDtos = allSkills.Select(skill =>
{
var section = sections.FirstOrDefault(s => s.SkillId == skill.Id);
if (section == null)
{
// اگر section وجود نداشت، یک DTO با وضعیت Unassigned برمی‌گردانیم
return new GetTaskSectionDto
{
Id = Guid.Empty,
SkillName = skill.Name ?? string.Empty,
SpentTime = TimeSpan.Zero,
TotalTime = TimeSpan.Zero,
Percentage = 0,
UserFullName = string.Empty,
AssignmentStatus = AssignmentStatus.Unassigned
};
}
// اگر section وجود داشت
return new GetTaskSectionDto
{
Id = section.Id,
SkillName = skill.Name ?? string.Empty,
SpentTime = TimeSpan.FromTicks(section.Activities.Sum(a => a.GetTimeSpent().Ticks)),
TotalTime = section.FinalEstimatedHours,
Percentage = (int)section.GetProgressPercentage(),
UserFullName = section.CurrentAssignedUserId > 0 && users.ContainsKey(section.CurrentAssignedUserId)
? users[section.CurrentAssignedUserId]
: string.Empty,
AssignmentStatus = GetAssignmentStatus(section)
};
}).ToList();
result.Add(new GetTaskDto
{ {
Id = task.Id, Id = task.Id,
Name = task.Name, Name = task.Name,
@@ -129,187 +185,176 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
ParentId = task.PhaseId, ParentId = task.PhaseId,
Percentage = percentage, Percentage = percentage,
TotalHours = (int)totalTime.TotalHours, TotalHours = (int)totalTime.TotalHours,
Minutes = totalTime.Minutes Minutes = totalTime.Minutes,
SpentTime = spentTime,
RemainingTime = remainingTime,
Sections = sectionDtos,
Priority = task.Priority
}); });
} }
return result; return result;
} }
private async Task SetSkillFlags(List<GetProjectListDto> projects, CancellationToken cancellationToken) private async Task SetSkillFlags<TItem>(List<TItem> items, CancellationToken cancellationToken) where TItem : GetProjectItemDto
{ {
if (!projects.Any()) if (!items.Any())
return; return;
var ids = items.Select(x => x.Id).ToList();
var projectIds = projects.Select(x => x.Id).ToList(); var hierarchyLevel = items.First().Level;
var hierarchyLevel = projects.First().Level;
switch (hierarchyLevel) switch (hierarchyLevel)
{ {
case ProjectHierarchyLevel.Project: case ProjectHierarchyLevel.Project:
await SetSkillFlagsForProjects(projects, projectIds, cancellationToken); await SetSkillFlagsForProjects(items, ids, cancellationToken);
break; break;
case ProjectHierarchyLevel.Phase: case ProjectHierarchyLevel.Phase:
await SetSkillFlagsForPhases(projects, projectIds, cancellationToken); await SetSkillFlagsForPhases(items, ids, cancellationToken);
break;
case ProjectHierarchyLevel.Task:
await SetSkillFlagsForTasks(projects, projectIds, cancellationToken);
break; break;
} }
} }
private async Task SetSkillFlagsForProjects(List<GetProjectListDto> projects, List<Guid> projectIds, CancellationToken cancellationToken)
private async Task SetSkillFlagsForProjects<TItem>(List<TItem> items, List<Guid> projectIds, CancellationToken cancellationToken) where TItem : GetProjectItemDto
{ {
var projectSections = await _context.ProjectSections // For projects: gather all phases, then tasks, then sections
.Include(x => x.Skill) var phases = await _context.ProjectPhases
.Where(s => projectIds.Contains(s.ProjectId)) .Where(ph => projectIds.Contains(ph.ProjectId))
.Select(ph => ph.Id)
.ToListAsync(cancellationToken);
var tasks = await _context.ProjectTasks
.Where(t => phases.Contains(t.PhaseId))
.Select(t => t.Id)
.ToListAsync(cancellationToken);
var sections = await _context.TaskSections
.Include(s => s.Skill)
.Where(s => tasks.Contains(s.TaskId))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (!projectSections.Any()) foreach (var item in items)
return;
foreach (var project in projects)
{ {
var sections = projectSections.Where(s => s.ProjectId == project.Id).ToList(); var relatedPhases = phases; // used for filtering tasks by project
project.HasBackend = sections.Any(x => x.Skill?.Name == "Backend"); var relatedTasks = await _context.ProjectTasks
project.HasFront = sections.Any(x => x.Skill?.Name == "Frontend"); .Where(t => t.PhaseId != Guid.Empty && relatedPhases.Contains(t.PhaseId))
project.HasDesign = sections.Any(x => x.Skill?.Name == "UI/UX Design"); .Select(t => t.Id)
.ToListAsync(cancellationToken);
var itemSections = sections.Where(s => relatedTasks.Contains(s.TaskId));
item.Backend = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "Backend"));
item.Front = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "Frontend"));
item.Design = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "UI/UX Design"));
} }
} }
private async Task SetSkillFlagsForPhases(List<GetProjectListDto> projects, List<Guid> phaseIds, CancellationToken cancellationToken) private async Task SetSkillFlagsForPhases<TItem>(List<TItem> items, List<Guid> phaseIds, CancellationToken cancellationToken) where TItem : GetProjectItemDto
{ {
var phaseSections = await _context.PhaseSections // For phases: gather tasks, then sections
.Include(x => x.Skill) var tasks = await _context.ProjectTasks
.Where(s => phaseIds.Contains(s.PhaseId)) .Where(t => phaseIds.Contains(t.PhaseId))
.Select(t => t.Id)
.ToListAsync(cancellationToken);
var sections = await _context.TaskSections
.Include(s => s.Skill)
.Where(s => tasks.Contains(s.TaskId))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (!phaseSections.Any()) foreach (var item in items)
return;
foreach (var phase in projects)
{ {
var sections = phaseSections.Where(s => s.PhaseId == phase.Id).ToList(); // Filter tasks for this phase
phase.HasBackend = sections.Any(x => x.Skill?.Name == "Backend"); var phaseTaskIds = await _context.ProjectTasks
phase.HasFront = sections.Any(x => x.Skill?.Name == "Frontend"); .Where(t => t.PhaseId == item.Id)
phase.HasDesign = sections.Any(x => x.Skill?.Name == "UI/UX Design"); .Select(t => t.Id)
} .ToListAsync(cancellationToken);
} var itemSections = sections.Where(s => phaseTaskIds.Contains(s.TaskId));
item.Backend = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "Backend"));
private async Task SetSkillFlagsForTasks(List<GetProjectListDto> projects, List<Guid> taskIds, CancellationToken cancellationToken) item.Front = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "Frontend"));
{ item.Design = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "UI/UX Design"));
var taskSections = await _context.TaskSections
.Include(x => x.Skill)
.Where(s => taskIds.Contains(s.TaskId))
.ToListAsync(cancellationToken);
if (!taskSections.Any())
return;
foreach (var task in projects)
{
var sections = taskSections.Where(s => s.TaskId == task.Id).ToList();
task.HasBackend = sections.Any(x => x.Skill?.Name == "Backend");
task.HasFront = sections.Any(x => x.Skill?.Name == "Frontend");
task.HasDesign = sections.Any(x => x.Skill?.Name == "UI/UX Design");
} }
} }
private async Task<(int Percentage, TimeSpan TotalTime)> CalculateProjectPercentage(Project project, CancellationToken cancellationToken) private async Task<(int Percentage, TimeSpan TotalTime)> CalculateProjectPercentage(Project project, CancellationToken cancellationToken)
{ {
// گرفتن تمام فازهای پروژه
var phases = await _context.ProjectPhases var phases = await _context.ProjectPhases
.Where(ph => ph.ProjectId == project.Id) .Where(ph => ph.ProjectId == project.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (!phases.Any()) if (!phases.Any())
return (0, TimeSpan.Zero); return (0, TimeSpan.Zero);
// محاسبه درصد هر فاز و میانگین‌گیری
var phasePercentages = new List<int>(); var phasePercentages = new List<int>();
var totalTime = TimeSpan.Zero; var totalTime = TimeSpan.Zero;
foreach (var phase in phases) foreach (var phase in phases)
{ {
var (phasePercentage, phaseTime) = await CalculatePhasePercentage(phase, cancellationToken); var (phasePercentage, phaseTime) = await CalculatePhasePercentage(phase, cancellationToken);
phasePercentages.Add(phasePercentage); phasePercentages.Add(phasePercentage);
totalTime += phaseTime; totalTime += phaseTime;
} }
var averagePercentage = phasePercentages.Any() ? (int)phasePercentages.Average() : 0; var averagePercentage = phasePercentages.Any() ? (int)phasePercentages.Average() : 0;
return (averagePercentage, totalTime); return (averagePercentage, totalTime);
} }
private async Task<(int Percentage, TimeSpan TotalTime)> CalculatePhasePercentage(ProjectPhase phase, CancellationToken cancellationToken) private async Task<(int Percentage, TimeSpan TotalTime)> CalculatePhasePercentage(ProjectPhase phase, CancellationToken cancellationToken)
{ {
// گرفتن تمام تسک‌های فاز
var tasks = await _context.ProjectTasks var tasks = await _context.ProjectTasks
.Where(t => t.PhaseId == phase.Id) .Where(t => t.PhaseId == phase.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (!tasks.Any()) if (!tasks.Any())
return (0, TimeSpan.Zero); return (0, TimeSpan.Zero);
// محاسبه درصد هر تسک و میانگین‌گیری
var taskPercentages = new List<int>(); var taskPercentages = new List<int>();
var totalTime = TimeSpan.Zero; var totalTime = TimeSpan.Zero;
foreach (var task in tasks) foreach (var task in tasks)
{ {
var (taskPercentage, taskTime) = await CalculateTaskPercentage(task, cancellationToken); var (taskPercentage, taskTime) = await CalculateTaskPercentage(task, cancellationToken);
taskPercentages.Add(taskPercentage); taskPercentages.Add(taskPercentage);
totalTime += taskTime; totalTime += taskTime;
} }
var averagePercentage = taskPercentages.Any() ? (int)taskPercentages.Average() : 0; var averagePercentage = taskPercentages.Any() ? (int)taskPercentages.Average() : 0;
return (averagePercentage, totalTime); return (averagePercentage, totalTime);
} }
private async Task<(int Percentage, TimeSpan TotalTime)> CalculateTaskPercentage(ProjectTask task, CancellationToken cancellationToken) private async Task<(int Percentage, TimeSpan TotalTime)> CalculateTaskPercentage(ProjectTask task, CancellationToken cancellationToken)
{ {
// گرفتن تمام سکشن‌های تسک با activities
var sections = await _context.TaskSections var sections = await _context.TaskSections
.Include(s => s.Activities) .Include(s => s.Activities)
.Include(x=>x.AdditionalTimes) .Include(x=>x.AdditionalTimes)
.Where(s => s.TaskId == task.Id) .Where(s => s.TaskId == task.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (!sections.Any()) if (!sections.Any())
return (0, TimeSpan.Zero); return (0, TimeSpan.Zero);
// محاسبه درصد هر سکشن و میانگین‌گیری
var sectionPercentages = new List<int>(); var sectionPercentages = new List<int>();
var totalTime = TimeSpan.Zero; var totalTime = TimeSpan.Zero;
foreach (var section in sections) foreach (var section in sections)
{ {
var (sectionPercentage, sectionTime) = CalculateSectionPercentage(section); var (sectionPercentage, sectionTime) = CalculateSectionPercentage(section);
sectionPercentages.Add(sectionPercentage); sectionPercentages.Add(sectionPercentage);
totalTime += sectionTime; totalTime += sectionTime;
} }
var averagePercentage = sectionPercentages.Any() ? (int)sectionPercentages.Average() : 0; var averagePercentage = sectionPercentages.Any() ? (int)sectionPercentages.Average() : 0;
return (averagePercentage, totalTime); return (averagePercentage, totalTime);
} }
private static (int Percentage, TimeSpan TotalTime) CalculateSectionPercentage(TaskSection section) private static (int Percentage, TimeSpan TotalTime) CalculateSectionPercentage(TaskSection section)
{ {
// محاسبه کل زمان تخمین زده شده (اولیه + اضافی) return ((int)section.GetProgressPercentage(),section.FinalEstimatedHours);
var totalEstimatedHours = section.FinalEstimatedHours.TotalHours; }
// محاسبه کل زمان صرف شده از activities private static AssignmentStatus GetAssignmentStatus(TaskSection? section)
var totalSpentTime = TimeSpan.FromHours(section.Activities.Sum(a => a.GetTimeSpent().TotalHours)); {
// تعیین تکلیف نشده: section وجود ندارد
if (section == null)
return AssignmentStatus.Unassigned;
if (totalEstimatedHours <= 0) // بررسی وجود user
return (0, section.FinalEstimatedHours); bool hasUser = section.CurrentAssignedUserId > 0;
var totalSpentHours = totalSpentTime.TotalHours; // بررسی وجود time (InitialEstimatedHours بزرگتر از صفر باشد)
bool hasTime = section.InitialEstimatedHours > TimeSpan.Zero;
// محاسبه درصد (حداکثر 100%) // تعیین تکلیف شده: هم user و هم time تعیین شده
var percentage = (totalSpentHours / totalEstimatedHours) * 100; if (hasUser && hasTime)
return (Math.Min((int)Math.Round(percentage), 100), section.FinalEstimatedHours); return AssignmentStatus.Assigned;
// فقط کاربر تعیین شده: user دارد ولی time ندارد
if (hasUser && !hasTime)
return AssignmentStatus.UserOnly;
// تعیین تکلیف نشده: نه user دارد نه time
return AssignmentStatus.Unassigned;
} }
} }

View File

@@ -1,5 +1,6 @@
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList; namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
public record GetProjectsListResponse( public record GetProjectsListResponse(
List<GetProjectListDto> Projects); List<GetProjectDto> Projects,
List<GetPhaseDto> Phases,
List<GetTaskDto> Tasks);

View File

@@ -0,0 +1,32 @@
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
public class GetTaskDto
{
public Guid Id { get; init; }
public string Name { get; init; } = string.Empty;
public int Percentage { get; init; }
public ProjectHierarchyLevel Level { get; init; }
public Guid? ParentId { get; init; }
public int TotalHours { get; set; }
public int Minutes { get; set; }
// Task-specific fields
public TimeSpan SpentTime { get; init; }
public TimeSpan RemainingTime { get; init; }
public ProjectTaskPriority Priority { get; set; }
public List<GetTaskSectionDto> Sections { get; init; }
}
public class GetTaskSectionDto
{
public Guid Id { get; init; }
public string SkillName { get; init; } = string.Empty;
public TimeSpan SpentTime { get; init; }
public TimeSpan TotalTime { get; init; }
public int Percentage { get; init; }
public string UserFullName{ get; init; } = string.Empty;
public AssignmentStatus AssignmentStatus { get; set; }
}

View File

@@ -6,5 +6,6 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.Project
public record ProjectBoardListQuery: IBaseQuery<List<ProjectBoardListResponse>> public record ProjectBoardListQuery: IBaseQuery<List<ProjectBoardListResponse>>
{ {
public long? UserId { get; set; }
public TaskSectionStatus? Status { get; set; } public TaskSectionStatus? Status { get; set; }
} }

View File

@@ -41,6 +41,11 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
queryable = queryable.Where(x => x.Status == request.Status); queryable = queryable.Where(x => x.Status == request.Status);
} }
if (request.UserId is > 0)
{
queryable = queryable.Where(x => x.CurrentAssignedUserId == request.UserId);
}
var data = await queryable.ToListAsync(cancellationToken); var data = await queryable.ToListAsync(cancellationToken);
var activityUserIds = data.SelectMany(x => x.Activities).Select(a => a.UserId).Distinct().ToList(); var activityUserIds = data.SelectMany(x => x.Activities).Select(a => a.UserId).Distinct().ToList();
@@ -54,6 +59,9 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
var result = data var result = data
.OrderByDescending(x => x.CurrentAssignedUserId == currentUserId)
.ThenByDescending(x=>x.Task.Priority)
.ThenBy(x => GetStatusOrder(x.Status))
.Select(x => .Select(x =>
{ {
// محاسبه یکبار برای هر Activity و Cache کردن نتیجه // محاسبه یکبار برای هر Activity و Cache کردن نتیجه
@@ -95,9 +103,6 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
}); });
} }
} }
mergedHistories = mergedHistories.OrderByDescending(h => h.IsCurrentUser).ToList();
return new ProjectBoardListResponse() return new ProjectBoardListResponse()
{ {
Id = x.Id, Id = x.Id,
@@ -105,9 +110,11 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
ProjectName = x.Task.Phase.Project.Name, ProjectName = x.Task.Phase.Project.Name,
TaskName = x.Task.Name, TaskName = x.Task.Name,
SectionStatus = x.Status, SectionStatus = x.Status,
TaskPriority = x.Task.Priority,
Progress = new ProjectProgressDto() Progress = new ProjectProgressDto()
{ {
CompleteSecond = x.FinalEstimatedHours.TotalSeconds, CompleteSecond = x.FinalEstimatedHours.TotalSeconds,
Percentage = (int)x.GetProgressPercentage(),
CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds), CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds),
Histories = mergedHistories Histories = mergedHistories
}, },
@@ -117,19 +124,21 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
SkillName = x.Skill?.Name??"-", SkillName = x.Skill?.Name??"-",
TaskId = x.TaskId TaskId = x.TaskId
}; };
}) }).ToList();
.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); return OperationResult<List<ProjectBoardListResponse>>.Success(result);
} }
private static int GetStatusOrder(TaskSectionStatus status)
{
return status switch
{
TaskSectionStatus.InProgress => 0,
TaskSectionStatus.Incomplete => 1,
TaskSectionStatus.NotAssigned => 2,
TaskSectionStatus.ReadyToStart => 2,
TaskSectionStatus.PendingForCompletion => 3,
_ => 99
};
}
} }

View File

@@ -13,13 +13,14 @@ public class ProjectBoardListResponse
public string? AssignedUser { get; set; } public string? AssignedUser { get; set; }
public string OriginalUser { get; set; } public string OriginalUser { get; set; }
public string SkillName { get; set; } public string SkillName { get; set; }
public ProjectTaskPriority TaskPriority { get; set; }
public Guid TaskId { get; set; } public Guid TaskId { get; set; }
} }
public class ProjectProgressDto public class ProjectProgressDto
{ {
public double CurrentSecond { get; set; } public double CurrentSecond { get; set; }
public int Percentage { get; set; }
public double CompleteSecond { get; set; } public double CompleteSecond { get; set; }
public List<ProjectProgressHistoryDto> Histories { get; set; } public List<ProjectProgressHistoryDto> Histories { get; set; }
} }

View File

@@ -12,14 +12,16 @@ public record ProjectDeployBoardDetailsResponse(
public record ProjectDeployBoardDetailPhaseItem( public record ProjectDeployBoardDetailPhaseItem(
string Name, string Name,
TimeSpan TotalTimeSpan, TimeSpan TotalTimeSpan,
TimeSpan DoneTimeSpan); TimeSpan DoneTimeSpan,
int Percentage);
public record ProjectDeployBoardDetailTaskItem( public record ProjectDeployBoardDetailTaskItem(
string Name, string Name,
TimeSpan TotalTimeSpan, TimeSpan TotalTimeSpan,
TimeSpan DoneTimeSpan, TimeSpan DoneTimeSpan,
int Percentage,
List<ProjectDeployBoardDetailItemSkill> Skills) List<ProjectDeployBoardDetailItemSkill> Skills)
: ProjectDeployBoardDetailPhaseItem(Name, TotalTimeSpan, DoneTimeSpan); : ProjectDeployBoardDetailPhaseItem(Name, TotalTimeSpan, DoneTimeSpan, Percentage);
public record ProjectDeployBoardDetailItemSkill(string OriginalUserFullName, string SkillName, int TimePercentage); public record ProjectDeployBoardDetailItemSkill(string OriginalUserFullName, string SkillName, int TimePercentage);
@@ -71,6 +73,7 @@ public class
var doneTime = t.Sections.Aggregate(TimeSpan.Zero, var doneTime = t.Sections.Aggregate(TimeSpan.Zero,
(sum, next) => sum.Add(next.GetTotalTimeSpent())); (sum, next) => sum.Add(next.GetTotalTimeSpent()));
var skills = t.Sections var skills = t.Sections
.Select(s => .Select(s =>
{ {
@@ -79,11 +82,7 @@ public class
var skillName = s.Skill?.Name ?? "بدون مهارت"; var skillName = s.Skill?.Name ?? "بدون مهارت";
var totalTimeSpent = s.GetTotalTimeSpent(); var timePercentage = (int)s.GetProgressPercentage();
var timePercentage = s.FinalEstimatedHours.Ticks > 0
? (int)((totalTimeSpent.Ticks / (double)s.FinalEstimatedHours.Ticks) * 100)
: 0;
return new ProjectDeployBoardDetailItemSkill( return new ProjectDeployBoardDetailItemSkill(
originalUserFullName, originalUserFullName,
@@ -91,10 +90,22 @@ public class
timePercentage); timePercentage);
}).ToList(); }).ToList();
int taskPercentage;
if (skills.Count == 0)
{
taskPercentage = 0;
}
else
{
taskPercentage = (int)Math.Round(skills.Average(x => x.TimePercentage));
}
return new ProjectDeployBoardDetailTaskItem( return new ProjectDeployBoardDetailTaskItem(
t.Name, t.Name,
totalTime, totalTime,
doneTime, doneTime,
taskPercentage,
skills); skills);
}).ToList(); }).ToList();
@@ -104,7 +115,10 @@ public class
var doneTimeSpan = tasksRes.Aggregate(TimeSpan.Zero, var doneTimeSpan = tasksRes.Aggregate(TimeSpan.Zero,
(sum, next) => sum.Add(next.DoneTimeSpan)); (sum, next) => sum.Add(next.DoneTimeSpan));
var phaseRes = new ProjectDeployBoardDetailPhaseItem(phase.Name, totalTimeSpan, doneTimeSpan); var phasePercentage = tasksRes.Average(x => x.Percentage);
var phaseRes = new ProjectDeployBoardDetailPhaseItem(phase.Name, totalTimeSpan, doneTimeSpan,
(int)phasePercentage);
var res = new ProjectDeployBoardDetailsResponse(phaseRes, tasksRes); var res = new ProjectDeployBoardDetailsResponse(phaseRes, tasksRes);

View File

@@ -17,6 +17,7 @@ public record ProjectDeployBoardListItem()
public int DoneTasks { get; set; } public int DoneTasks { get; set; }
public TimeSpan TotalTimeSpan { get; set; } public TimeSpan TotalTimeSpan { get; set; }
public TimeSpan DoneTimeSpan { get; set; } public TimeSpan DoneTimeSpan { get; set; }
public int Percentage { get; set; }
public ProjectDeployStatus DeployStatus { get; set; } public ProjectDeployStatus DeployStatus { get; set; }
} }
public record GetProjectsDeployBoardListResponse(List<ProjectDeployBoardListItem> Items); public record GetProjectsDeployBoardListResponse(List<ProjectDeployBoardListItem> Items);
@@ -66,7 +67,8 @@ public class ProjectDeployBoardListQueryHandler:IBaseQueryHandler<GetProjectDepl
.Select(x => x.TaskId).Distinct().Count(), .Select(x => x.TaskId).Distinct().Count(),
TotalTimeSpan = TimeSpan.FromTicks(g.Sum(x => x.InitialEstimatedHours.Ticks)), TotalTimeSpan = TimeSpan.FromTicks(g.Sum(x => x.InitialEstimatedHours.Ticks)),
DoneTimeSpan = TimeSpan.FromTicks(g.Sum(x=>x.GetTotalTimeSpent().Ticks)), DoneTimeSpan = TimeSpan.FromTicks(g.Sum(x=>x.GetTotalTimeSpent().Ticks)),
DeployStatus = g.First().Task.Phase.DeployStatus DeployStatus = g.First().Task.Phase.DeployStatus,
Percentage = (int)Math.Round(g.Average(x => x.GetProgressPercentage()))
}).ToList(); }).ToList();
var response = new GetProjectsDeployBoardListResponse(list); var response = new GetProjectsDeployBoardListResponse(list);
return OperationResult<GetProjectsDeployBoardListResponse>.Success(response); return OperationResult<GetProjectsDeployBoardListResponse>.Success(response);

View File

@@ -1,15 +1,12 @@
using GozareshgirProgramManager.Application._Common.Interfaces; using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models; using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs; using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
using GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetMessages; namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetMessages;
public record GetMessagesQuery( public record GetMessagesQuery(
Guid TaskId, Guid TaskId,
MessageType? MessageType,
int Page = 1, int Page = 1,
int PageSize = 50 int PageSize = 50
) : IBaseQuery<PaginationResult<MessageDto>>; ) : IBaseQuery<PaginationResult<MessageDto>>;
@@ -27,6 +24,39 @@ public class GetMessagesQueryHandler : IBaseQueryHandler<GetMessagesQuery, Pagin
_authHelper = authHelper; _authHelper = authHelper;
} }
private List<MessageDto> CreateAdditionalTimeNotes(
IEnumerable<Domain.ProjectAgg.Entities.TaskSectionAdditionalTime> additionalTimes,
Dictionary<long, string> users,
Guid taskId)
{
var notes = new List<MessageDto>();
foreach (var additionalTime in additionalTimes)
{
var addedByUserName = additionalTime.AddedByUserId.HasValue && users.TryGetValue(additionalTime.AddedByUserId.Value, out var user)
? user
: "سیستم";
var noteContent = $"⏱️ زمان اضافی: {additionalTime.Hours.TotalHours.ToString("F2")} ساعت - {(string.IsNullOrWhiteSpace(additionalTime.Reason) ? "بدون علت" : additionalTime.Reason)} - توسط {addedByUserName}";
var noteDto = new MessageDto
{
Id = Guid.NewGuid(),
TaskId = taskId,
SenderUserId = 0,
SenderName = "سیستم",
MessageType = "Note",
TextContent = noteContent,
CreationDate = additionalTime.CreationDate,
IsMine = false
};
notes.Add(noteDto);
}
return notes;
}
public async Task<OperationResult<PaginationResult<MessageDto>>> Handle(GetMessagesQuery request, CancellationToken cancellationToken) public async Task<OperationResult<PaginationResult<MessageDto>>> Handle(GetMessagesQuery request, CancellationToken cancellationToken)
{ {
var currentUserId = _authHelper.GetCurrentUserId(); var currentUserId = _authHelper.GetCurrentUserId();
@@ -36,12 +66,7 @@ public class GetMessagesQueryHandler : IBaseQueryHandler<GetMessagesQuery, Pagin
var query = _context.TaskChatMessages var query = _context.TaskChatMessages
.Where(m => m.TaskId == request.TaskId && !m.IsDeleted) .Where(m => m.TaskId == request.TaskId && !m.IsDeleted)
.Include(m => m.ReplyToMessage) .Include(m => m.ReplyToMessage)
.OrderBy(m => m.CreationDate).AsQueryable(); .OrderBy(m => m.CreationDate);
if (request.MessageType.HasValue)
{
query = query.Where(m => m.MessageType == request.MessageType.Value);
}
var totalCount = await query.CountAsync(cancellationToken); var totalCount = await query.CountAsync(cancellationToken);
@@ -51,36 +76,52 @@ public class GetMessagesQueryHandler : IBaseQueryHandler<GetMessagesQuery, Pagin
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
// ✅ گرفتن تمامی کاربران برای نمایش نام کامل فرستنده به جای "کاربر" // ✅ گرفتن تمامی کاربران برای نمایش نام کامل فرستنده به جای "کاربر"
// این بخش تمام UserId هایی که در پیام‌ها استفاده شده را جمع‌آوری می‌کند
// و یک Dictionary ایجاد می‌کند که UserId را به FullName نگاشت می‌کند
var senderUserIds = messages.Select(m => m.SenderUserId).Distinct().ToList(); var senderUserIds = messages.Select(m => m.SenderUserId).Distinct().ToList();
var users = await _context.Users var users = await _context.Users
.Where(u => senderUserIds.Contains(u.Id)) .Where(u => senderUserIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken); .ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
// ✅ گرفتن تمامی زمان‌های اضافی (Additional Times) برای نمایش به صورت نوت // ✅ گرفتن تمامی زمان‌های اضافی (Additional Times) برای نمایش به صورت نوت
// در اینجا تمامی TaskSections مربوط به این تسک را می‌گیریم
// و برای هر کدام تمام AdditionalTimes آن را بارگذاری می‌کنیم
var taskSections = await _context.TaskSections var taskSections = await _context.TaskSections
.Where(ts => ts.TaskId == request.TaskId) .Where(ts => ts.TaskId == request.TaskId)
.Include(ts => ts.AdditionalTimes) .Include(ts => ts.AdditionalTimes)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
// ✅ تمام زمان‌های اضافی را یکجا بگیر و مرتب کن
var allAdditionalTimes = taskSections
.SelectMany(ts => ts.AdditionalTimes)
.OrderBy(at => at.CreationDate)
.ToList();
var messageDtos = new List<MessageDto>(); var messageDtos = new List<MessageDto>();
// ✅ ابتدا زمان‌های اضافی قبل از اولین پیام را اضافه کن (اگر پیامی وجود داشته باشد)
if (messages.Any())
{
var firstMessageDate = messages.First().CreationDate;
var additionalTimesBeforeFirstMessage = allAdditionalTimes
.Where(at => at.CreationDate < firstMessageDate)
.ToList();
messageDtos.AddRange(CreateAdditionalTimeNotes(additionalTimesBeforeFirstMessage, users, request.TaskId));
}
else
{
// ✅ اگر هیچ پیامی وجود ندارد، همه زمان‌های اضافی را نمایش بده
messageDtos.AddRange(CreateAdditionalTimeNotes(allAdditionalTimes, users, request.TaskId));
}
foreach (var message in messages) foreach (var message in messages)
{ {
// ✅ نام فرستنده را از Dictionary Users بگیر، در صورت عدم وجود "کاربر ناشناس" نمایش بده // ✅ نام فرستنده را از Dictionary Users بگیر، در صورت عدم وجود "کاربر ناشناس" نمایش بده
var senderName = users.ContainsKey(message.SenderUserId) var senderName = users.GetValueOrDefault(message.SenderUserId, "کاربر ناشناس");
? users[message.SenderUserId]
: "کاربر ناشناس";
var dto = new MessageDto var dto = new MessageDto
{ {
Id = message.Id, Id = message.Id,
TaskId = message.TaskId, TaskId = message.TaskId,
SenderUserId = message.SenderUserId, SenderUserId = message.SenderUserId,
SenderName = senderName, // ✅ از User واقعی استفاده می‌کنیم SenderName = senderName,
MessageType = message.MessageType.ToString(), MessageType = message.MessageType.ToString(),
TextContent = message.TextContent, TextContent = message.TextContent,
ReplyToMessageId = message.ReplyToMessageId, ReplyToMessageId = message.ReplyToMessageId,
@@ -95,10 +136,7 @@ public class GetMessagesQueryHandler : IBaseQueryHandler<GetMessagesQuery, Pagin
if (message.ReplyToMessage != null) if (message.ReplyToMessage != null)
{ {
// ✅ برای پیام‌های Reply نیز نام فرستنده را درست نمایش بده var replySenderName = users.GetValueOrDefault(message.ReplyToMessage.SenderUserId, "کاربر ناشناس");
var replySenderName = users.ContainsKey(message.ReplyToMessage.SenderUserId)
? users[message.ReplyToMessage.SenderUserId]
: "کاربر ناشناس";
dto.ReplyToMessage = new MessageDto dto.ReplyToMessage = new MessageDto
{ {
@@ -132,55 +170,31 @@ public class GetMessagesQueryHandler : IBaseQueryHandler<GetMessagesQuery, Pagin
messageDtos.Add(dto); messageDtos.Add(dto);
// ✅ اینجا بخش جدید است: نوت‌های زمان اضافی را بین پیام‌ها اضافه کن // ✅ پیدا کردن پیام بعدی (اگر وجود داشته باشد)
// این بخش تمام AdditionalTimes را که بعد از این پیام اضافه شده‌اند را پیدا می‌کند var currentIndex = messages.IndexOf(message);
var additionalTimesAfterMessage = taskSections var nextMessage = currentIndex < messages.Count - 1 ? messages[currentIndex + 1] : null;
.SelectMany(ts => ts.AdditionalTimes)
.Where(at => at.AddedAt > message.CreationDate) // ✅ تغییر به AddedAt (زمان واقعی اضافه شدن)
.OrderBy(at => at.AddedAt)
.FirstOrDefault();
if (additionalTimesAfterMessage != null) if (nextMessage != null)
{ {
// ✅ تمام AdditionalTimes بین این پیام و پیام قبلی را بگیر // ✅ زمان‌های اضافی بین این پیام و پیام بعدی
var additionalTimesByDate = taskSections var additionalTimesBetween = allAdditionalTimes
.SelectMany(ts => ts.AdditionalTimes) .Where(at => at.CreationDate > message.CreationDate && at.CreationDate < nextMessage.CreationDate)
.Where(at => at.AddedAt <= message.CreationDate &&
(messageDtos.Count == 1 || at.AddedAt > messageDtos[messageDtos.Count - 2].CreationDate))
.OrderBy(at => at.AddedAt)
.ToList(); .ToList();
foreach (var additionalTime in additionalTimesByDate) messageDtos.AddRange(CreateAdditionalTimeNotes(additionalTimesBetween, users, request.TaskId));
{ }
// ✅ نام کاربری که این زمان اضافی را اضافه کرد else
var addedByUserName = additionalTime.AddedByUserId.HasValue && users.TryGetValue(additionalTime.AddedByUserId.Value, out var user) {
? user // ✅ این آخرین پیام است، زمان‌های اضافی بعد از آن را اضافه کن
: "سیستم"; var additionalTimesAfterLastMessage = allAdditionalTimes
.Where(at => at.CreationDate > message.CreationDate)
.ToList();
// ✅ محتوای نوت را با اطلاعات کامل ایجاد کن messageDtos.AddRange(CreateAdditionalTimeNotes(additionalTimesAfterLastMessage, users, request.TaskId));
// نمایش می‌دهد: مقدار زمان + علت + نام کسی که اضافه کرد
var noteContent = $"⏱️ زمان اضافی: {additionalTime.Hours.TotalHours:F2} ساعت - {(string.IsNullOrWhiteSpace(additionalTime.Reason) ? "بدون علت" : additionalTime.Reason)} - توسط {addedByUserName}";
// ✅ نوت را به عنوان MessageDto خاصی ایجاد کن
var noteDto = new MessageDto
{
Id = Guid.NewGuid(),
TaskId = request.TaskId,
SenderUserId = 0, // ✅ سیستم برای نشان دادن اینکه یک پیام خودکار است
SenderName = "سیستم",
MessageType = "Note", // ✅ نوع پیام: Note (یادداشت سیستم)
TextContent = noteContent,
CreationDate = additionalTime.AddedAt, // ✅ تاریخ اضافه شدن زمان اضافی
IsMine = false
};
messageDtos.Add(noteDto);
}
} }
} }
// ✅ مرتب کردن نهایی تمام پیام‌ها (معمولی + نوت‌ها) بر اساس زمان ایجاد // ✅ مرتب کردن نهایی تمام پیام‌ها (معمولی + نوت‌ها) بر اساس زمان ایجاد
// اینطور که نوت‌های زمان اضافی در جای درست خود قرار می‌گیرند
messageDtos = messageDtos.OrderBy(m => m.CreationDate).ToList(); messageDtos = messageDtos.OrderBy(m => m.CreationDate).ToList();
var response = new PaginationResult<MessageDto>() var response = new PaginationResult<MessageDto>()

View File

@@ -20,7 +20,7 @@ public class ProjectTask : ProjectHierarchyNode
{ {
PhaseId = phaseId; PhaseId = phaseId;
_sections = new List<TaskSection>(); _sections = new List<TaskSection>();
Priority = TaskPriority.Medium; Priority = ProjectTaskPriority.Medium;
AddDomainEvent(new TaskCreatedEvent(Id, phaseId, name)); AddDomainEvent(new TaskCreatedEvent(Id, phaseId, name));
} }
@@ -30,7 +30,7 @@ public class ProjectTask : ProjectHierarchyNode
// Task-specific properties // Task-specific properties
public Enums.TaskStatus Status { get; private set; } = Enums.TaskStatus.NotStarted; public Enums.TaskStatus Status { get; private set; } = Enums.TaskStatus.NotStarted;
public TaskPriority Priority { get; private set; } public ProjectTaskPriority Priority { get; private set; }
public DateTime? StartDate { get; private set; } public DateTime? StartDate { get; private set; }
public DateTime? EndDate { get; private set; } public DateTime? EndDate { get; private set; }
public DateTime? DueDate { get; private set; } public DateTime? DueDate { get; private set; }
@@ -119,7 +119,7 @@ public class ProjectTask : ProjectHierarchyNode
AddDomainEvent(new TaskStatusUpdatedEvent(Id, status)); AddDomainEvent(new TaskStatusUpdatedEvent(Id, status));
} }
public void SetPriority(TaskPriority priority) public void SetPriority(ProjectTaskPriority priority)
{ {
Priority = priority; Priority = priority;
AddDomainEvent(new TaskPriorityUpdatedEvent(Id, priority)); AddDomainEvent(new TaskPriorityUpdatedEvent(Id, priority));

View File

@@ -157,6 +157,27 @@ public class TaskSection : EntityBase<Guid>
return TimeSpan.FromTicks(_activities.Sum(a => a.GetTimeSpent().Ticks)); return TimeSpan.FromTicks(_activities.Sum(a => a.GetTimeSpent().Ticks));
} }
/// <summary>
/// محاسبه درصد پیشرفت بر اساس زمان مصرف شده به تایم برآورد شده
/// اگر وضعیت Completed باشد، همیشه 100 درصد برمی‌گرداند
/// </summary>
public double GetProgressPercentage()
{
// اگر تسک کامل شده، همیشه 100 درصد
if (Status == TaskSectionStatus.Completed)
return 100.0;
// اگر تایم برآورد شده صفر است، درصد صفر است
if (FinalEstimatedHours.TotalHours <= 0)
return 0.0;
var timeSpent = GetTotalTimeSpent();
var percentage = (timeSpent.TotalMinutes / FinalEstimatedHours.TotalMinutes) * 100.0;
// محدود کردن درصد به 100 (در صورتی که زمان مصرف شده بیشتر از تخمین باشد)
return Math.Min(percentage, 100.0);
}
public bool IsCompleted() public bool IsCompleted()
{ {
return Status == TaskSectionStatus.Completed; return Status == TaskSectionStatus.Completed;
@@ -249,7 +270,7 @@ public class TaskSection : EntityBase<Guid>
// متوقف کردن فعالیت با EndDate دقیق شده // متوقف کردن فعالیت با EndDate دقیق شده
activeActivity.StopWorkWithSpecificTime(adjustedEndDate, "متوقف خودکار - بیش از تایم تعیین شده"); activeActivity.StopWorkWithSpecificTime(adjustedEndDate, "متوقف خودکار - بیش از تایم تعیین شده");
UpdateStatus(TaskSectionStatus.Incomplete); UpdateStatus(TaskSectionStatus.PendingForCompletion);
} }
} }
} }

View File

@@ -0,0 +1,23 @@
namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
/// <summary>
/// وضعیت تکلیف دهی برای بخش‌های مختلف پروژه
/// </summary>
public enum AssignmentStatus
{
/// <summary>
/// تعیین تکلیف نشده
/// </summary>
Unassigned = 0,
/// <summary>
/// تعیین تکلیف شده
/// </summary>
Assigned = 1,
/// <summary>
/// فقط کاربر تعیین شده
/// </summary>
UserOnly = 2,
}

View File

@@ -3,7 +3,7 @@ namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
/// <summary> /// <summary>
/// اولویت تسک /// اولویت تسک
/// </summary> /// </summary>
public enum TaskPriority public enum ProjectTaskPriority
{ {
/// <summary> /// <summary>
/// پایین /// پایین

View File

@@ -78,7 +78,7 @@ public record TaskStatusUpdatedEvent(Guid TaskId, TaskStatus Status) : IDomainEv
public DateTime OccurredOn { get; init; } = DateTime.Now; public DateTime OccurredOn { get; init; } = DateTime.Now;
} }
public record TaskPriorityUpdatedEvent(Guid TaskId, TaskPriority Priority) : IDomainEvent public record TaskPriorityUpdatedEvent(Guid TaskId, ProjectTaskPriority Priority) : IDomainEvent
{ {
public DateTime OccurredOn { get; init; } = DateTime.Now; public DateTime OccurredOn { get; init; } = DateTime.Now;
} }

View File

@@ -36,7 +36,7 @@ public interface IProjectTaskRepository : IRepository<Guid, ProjectTask>
/// <summary> /// <summary>
/// Get tasks by priority /// Get tasks by priority
/// </summary> /// </summary>
Task<List<ProjectTask>> GetByPriorityAsync(ProjectAgg.Enums.TaskPriority priority); Task<List<ProjectTask>> GetByPriorityAsync(ProjectAgg.Enums.ProjectTaskPriority priority);
/// <summary> /// <summary>
/// Get tasks assigned to user /// Get tasks assigned to user

View File

@@ -14,4 +14,7 @@ public interface ITaskSectionRepository: IRepository<Guid,TaskSection>
Task<List<TaskSection>> GetAssignedToUserAsync(long userId); Task<List<TaskSection>> GetAssignedToUserAsync(long userId);
Task<List<TaskSection>> GetActiveSectionsIncludeAllAsync(CancellationToken cancellationToken); Task<List<TaskSection>> GetActiveSectionsIncludeAllAsync(CancellationToken cancellationToken);
Task<bool> HasUserAnyInProgressSectionAsync(long userId, CancellationToken cancellationToken = default); Task<bool> HasUserAnyInProgressSectionAsync(long userId, CancellationToken cancellationToken = default);
// جدید: دریافت سکشن‌هایی که هنوز Completed یا PendingForCompletion نشده‌اند با اطلاعات کامل
Task<List<TaskSection>> GetAllNotCompletedOrPendingIncludeAllAsync(CancellationToken cancellationToken);
} }

View File

@@ -15,6 +15,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<!--<PackageReference Include="System.Text.Encodings.Web" Version="10.0.0" />--> <!--<PackageReference Include="System.Text.Encodings.Web" Version="10.0.0" />-->

View File

@@ -58,7 +58,7 @@ public class ProjectTaskRepository : RepositoryBase<Guid, ProjectTask>, IProject
.ToListAsync(); .ToListAsync();
} }
public Task<List<ProjectTask>> GetByPriorityAsync(TaskPriority priority) public Task<List<ProjectTask>> GetByPriorityAsync(ProjectTaskPriority priority)
{ {
return _context.ProjectTasks return _context.ProjectTasks
.Where(t => t.Priority == priority) .Where(t => t.Priority == priority)

View File

@@ -19,6 +19,7 @@ public class TaskSectionRepository:RepositoryBase<Guid,TaskSection>,ITaskSection
{ {
return await _context.TaskSections return await _context.TaskSections
.Include(x => x.Activities) .Include(x => x.Activities)
.Include(x=>x.AdditionalTimes)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken); .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
} }
@@ -52,4 +53,13 @@ public class TaskSectionRepository:RepositoryBase<Guid,TaskSection>,ITaskSection
.AnyAsync(x => x.CurrentAssignedUserId == userId && x.Status == TaskSectionStatus.InProgress, .AnyAsync(x => x.CurrentAssignedUserId == userId && x.Status == TaskSectionStatus.InProgress,
cancellationToken); cancellationToken);
} }
public Task<List<TaskSection>> GetAllNotCompletedOrPendingIncludeAllAsync(CancellationToken cancellationToken)
{
return _context.TaskSections
.Where(x => x.Status != TaskSectionStatus.Completed && x.Status != TaskSectionStatus.PendingForCompletion)
.Include(x => x.Activities)
.Include(x => x.AdditionalTimes)
.ToListAsync(cancellationToken);
}
} }

View File

@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement; namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement;
/// <summary> /// <summary>

View File

@@ -22,6 +22,8 @@ using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectH
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using ServiceHost.BaseControllers; using ServiceHost.BaseControllers;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeTaskPriority;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoPendingFullTimeTaskSections;
namespace ServiceHost.Areas.Admin.Controllers.ProgramManager; namespace ServiceHost.Areas.Admin.Controllers.ProgramManager;
@@ -122,6 +124,8 @@ public class ProjectController : ProgramManagerBaseController
{ {
// اجرای Command برای متوقف کردن تسک‌های overtime قبل از نمایش // اجرای Command برای متوقف کردن تسک‌های overtime قبل از نمایش
await _mediator.Send(new AutoStopOverTimeTaskSectionsCommand()); await _mediator.Send(new AutoStopOverTimeTaskSectionsCommand());
// سپس تسک‌هایی که به 100% زمان تخمینی رسیده‌اند را به حالت PendingForCompletion ببریم
await _mediator.Send(new AutoPendingFullTimeTaskSectionsCommand());
var res = await _mediator.Send(query); var res = await _mediator.Send(query);
return res; return res;
@@ -165,4 +169,12 @@ public class ProjectController : ProgramManagerBaseController
var res = await _mediator.Send(command); var res = await _mediator.Send(command);
return res; return res;
} }
[HttpPost("change-priority")]
public async Task<ActionResult<OperationResult>> ChangePriority([FromBody] ChangeTaskPriorityCommand command)
{
var res = await _mediator.Send(command);
return res;
}
} }

View File

@@ -8,7 +8,6 @@ using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetMessages; using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetMessages;
using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetPinnedMessages; using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetPinnedMessages;
using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.SearchMessages; using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.SearchMessages;
using GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using ServiceHost.BaseControllers; using ServiceHost.BaseControllers;
@@ -31,17 +30,15 @@ public class TaskChatController : ProgramManagerBaseController
/// دریافت لیست پیام‌های یک تسک /// دریافت لیست پیام‌های یک تسک
/// </summary> /// </summary>
/// <param name="taskId">شناسه تسک</param> /// <param name="taskId">شناسه تسک</param>
/// <param name="messageType">نوع پیام</param>
/// <param name="page">صفحه (پیش‌فرض: 1)</param> /// <param name="page">صفحه (پیش‌فرض: 1)</param>
/// <param name="pageSize">تعداد در هر صفحه (پیش‌فرض: 50)</param> /// <param name="pageSize">تعداد در هر صفحه (پیش‌فرض: 50)</param>
[HttpGet("{taskId:guid}/messages")] [HttpGet("{taskId:guid}/messages")]
public async Task<ActionResult<OperationResult<PaginationResult<MessageDto>>>> GetMessages( public async Task<ActionResult<OperationResult<PaginationResult<MessageDto>>>> GetMessages(
Guid taskId, Guid taskId,
[FromQuery] MessageType? messageType,
[FromQuery] int page = 1, [FromQuery] int page = 1,
[FromQuery] int pageSize = 50) [FromQuery] int pageSize = 50)
{ {
var query = new GetMessagesQuery(taskId,messageType, page, pageSize); var query = new GetMessagesQuery(taskId, page, pageSize);
var result = await _mediator.Send(query); var result = await _mediator.Send(query);
return result; return result;
} }

View File

@@ -916,6 +916,17 @@ public class institutionContractController : AdminBaseController
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"قرارداد های مالی.xlsx"); $"قرارداد های مالی.xlsx");
} }
/// <summary>
/// تنظیم وضعیت ارسال قرارداد
/// </summary>
[HttpPost("set-is-sent")]
public async Task<ActionResult<OperationResult>> SetIsSent([FromBody] SetInstitutionContractSendFlagRequest request)
{
var result = await _institutionContractApplication.SetContractSendFlag(request);
return result;
}
} }
public class InstitutionContractCreationGetRepresentativeIdResponse public class InstitutionContractCreationGetRepresentativeIdResponse

View File

@@ -0,0 +1,59 @@
using _0_Framework.Application;
using _0_Framework.Domain.CustomizeCheckoutShared.Enums;
using CompanyManagment.App.Contracts.Loan;
using Microsoft.AspNetCore.Mvc;
using ServiceHost.BaseControllers;
namespace ServiceHost.Areas.Client.Controllers;
public class LoanController: ClientBaseController
{
private readonly ILoanApplication _loanApplication;
private readonly long _workshopId;
public LoanController(ILoanApplication loanApplication, IAuthHelper authHelper)
{
_loanApplication = loanApplication;
_workshopId= authHelper.GetWorkshopId();
}
[HttpGet]
public ActionResult<LoanGroupedViewModel> GetList(LoanSearchViewModel searchModel)
{
searchModel.WorkshopId = _workshopId;
var loans = _loanApplication.GetSearchListAsGrouped(searchModel);
return loans;
}
[HttpGet("{id}")]
public async Task<ActionResult<LoanDetailsViewModel>> GetDetails(long id)
{
var loan = await _loanApplication.GetDetails(id);
return loan;
}
[HttpPost]
public ActionResult<OperationResult> Create([FromBody] CreateLoanViewModel command)
{
var result = _loanApplication.Create(command);
return result;
}
[HttpGet("create/installments")]
public ActionResult<List<LoanInstallmentViewModel>> CalculateLoanInstallment(string amount,
int installmentCount, string loanStartDate, bool getRounded)
{
var installments =
_loanApplication.CalculateLoanInstallment(amount, installmentCount, loanStartDate, getRounded);
return installments;
}
[HttpDelete("{id}")]
public ActionResult<OperationResult> Remove(long id)
{
var result = _loanApplication.Remove(id);
return result;
}
}

View File

@@ -63,10 +63,17 @@ if (!Directory.Exists(logDirectory))
Directory.CreateDirectory(logDirectory); Directory.CreateDirectory(logDirectory);
} }
// فقط برای فایل از Serilog استفاده می‌شود
// تنظیمات MinimumLevel از appsettings.json خوانده می‌شود
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext() //NO EF Core log
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
//NO DbCommand log
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Warning)
//NO Microsoft Public log
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
//.MinimumLevel.Information()
.WriteTo.File( .WriteTo.File(
path: Path.Combine(logDirectory, "gozareshgir_log.txt"), path: Path.Combine(logDirectory, "gozareshgir_log.txt"),
rollingInterval: RollingInterval.Day, rollingInterval: RollingInterval.Day,
@@ -373,29 +380,30 @@ builder.Services.AddParbad().ConfigureGateways(gateways =>
}); });
// فقط Serilog برای File استفاده می‌شه، کنسول از لاگر پیش‌فرض ASP.NET استفاده می‌کنه if (builder.Environment.IsDevelopment())
builder.Host.UseSerilog((context, services, configuration) =>
{ {
var logConfig = configuration builder.Host.UseSerilog((context, services, configuration) =>
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext();
// در محیط Development، EF Core Commands را هم لاگ می‌کنیم
if (context.HostingEnvironment.IsDevelopment())
{ {
logConfig var logConfig = configuration
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Information); .ReadFrom.Configuration(context.Configuration)
} .ReadFrom.Services(services)
.Enrich.FromLogContext();
logConfig.WriteTo.File(
path: Path.Combine(logDirectory, "gozareshgir_log.txt"), logConfig.WriteTo.File(
rollingInterval: RollingInterval.Day, path: Path.Combine(logDirectory, "gozareshgir_log.txt"),
retainedFileCountLimit: 30, rollingInterval: RollingInterval.Day,
shared: true, retainedFileCountLimit: 30,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}" shared: true,
); outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}"
}, writeToProviders: true); // این باعث میشه کنسول پیش‌فرض هم کار کنه );
}, writeToProviders: true); // این باعث میشه کنسول پیش‌فرض هم کار کنه
}
else
{
builder.Host.UseSerilog();
}
Log.Information("SERILOG STARTED SUCCESSFULLY"); Log.Information("SERILOG STARTED SUCCESSFULLY");

View File

@@ -19,7 +19,7 @@
"sqlDebugging": true, "sqlDebugging": true,
"dotnetRunMessages": "true", "dotnetRunMessages": "true",
"nativeDebugging": true, "nativeDebugging": true,
"applicationUrl": "https://localhost:5004;http://localhost:5003;https://192.168.0.117:5006", "applicationUrl": "https://localhost:5004;http://localhost:5003;",
"jsWebView2Debugging": false, "jsWebView2Debugging": false,
"hotReloadEnabled": true "hotReloadEnabled": true
}, },
@@ -44,7 +44,7 @@
"sqlDebugging": true, "sqlDebugging": true,
"dotnetRunMessages": "true", "dotnetRunMessages": "true",
"nativeDebugging": true, "nativeDebugging": true,
"applicationUrl": "https://localhost:5004;http://localhost:5003;https://192.168.0.117:5006;", "applicationUrl": "https://localhost:5004;http://localhost:5003;",
"jsWebView2Debugging": false, "jsWebView2Debugging": false,
"hotReloadEnabled": true "hotReloadEnabled": true
} }

View File

@@ -10,6 +10,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<RazorCompileOnBuild>true</RazorCompileOnBuild> <RazorCompileOnBuild>true</RazorCompileOnBuild>
<UserSecretsId>a6049acf-0286-4947-983a-761d06d65f36</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<!--<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <!--<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

View File

@@ -1,77 +0,0 @@
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
//تست
//"MesbahDb": "Data Source=DESKTOP-NUE119G\\MSNEW;Initial Catalog=Mesbah_db;Integrated Security=True"
//server
"MesbahDbServer": "Data Source=171.22.24.15;Initial Catalog=mesbah_db;Persist Security Info=False;User ID=ir_db;Password=R2rNp[170]18[3019]#@ATt;TrustServerCertificate=true;",
//local
"MesbahDb": "Data Source=.;Initial Catalog=mesbah_db;Integrated Security=True;TrustServerCertificate=true;",
//server
//"MesbahDb": "Data Source=185.208.175.186;Initial Catalog=mesbah_db;Persist Security Info=False;User ID=ir_db;Password=R2rNp[170]18[3019]#@ATt;TrustServerCertificate=true;",
//dad-mehr
//"MesbahDb": "Data Source=.;Initial Catalog=teamWork;Integrated Security=True;TrustServerCertificate=true;",
"TestDb": "Data Source=.;Initial Catalog=TestDb;Integrated Security=True;TrustServerCertificate=true;",
//program_manager_db
"ProgramManagerDb": "Data Source=.;Initial Catalog=program_manager_db;Integrated Security=True;TrustServerCertificate=true;",
//"ProgramManagerDb": "Data Source=185.208.175.186;Initial Catalog=program_manager_db;Persist Security Info=False;User ID=ir_db;Password=R2rNp[170]18[3019]#@ATt;TrustServerCertificate=true;"
"ProgramManagerDbServer": "Data Source=171.22.24.15;Initial Catalog=program_manager_db;Persist Security Info=False;User ID=ir_db;Password=R2rNp[170]18[3019]#@ATt;TrustServerCertificate=true;"
//mahan Docker
//"MesbahDb": "Data Source=localhost,5069;Initial Catalog=mesbah_db;User ID=sa;Password=YourPassword123;TrustServerCertificate=True;"
},
"GoogleRecaptchaV3": {
"SiteKey": "6Lfhp_AnAAAAAB79WkrMoHd1k8ir4m8VvfjE7FTH",
"SecretKey": "6Lfhp_AnAAAAANjDDY6DPrbbUQS7k6ZCRmrVP5Lb"
},
"SmsSecrets": {
"ApiKey": "Og5M562igmzJRhQPnq0GdtieYdLgtfikjzxOmeQBPxJjZtyge5Klc046Lfw1mxSa",
"SecretKey": "dadmehr"
},
"Domain": ".dadmehrg.ir",
"MongoDb": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "Gozareshgir"
},
"SmsSettings": {
"IsTestMode": true,
"TestNumbers": [
"09116967898"
//, "09116067106", "09114221321"
]
},
"InternalApi": {
"Local": "https://localhost:7032",
"Dadmehrg": "https://api.pm.dadmehrg.ir",
"Gozareshgir": "https://api.pm.gozareshgir.ir"
},
"SepehrGateWayTerminalId": 99213700,
"JwtSettings": {
"SecretKey": ">3£>^1UBG@yw)QdhRC3$£:;r8~?qpp^oKK4D3a~8L2>enF;lkgh",
"Issuer": "GozareshgirApp",
"Audience": "GozareshgirUsers",
"ExpirationMinutes": 30
},
"FileStorage": {
"BaseUrl": "https://localhost:7032/uploads",
"MaxFileSizeBytes": 104857600,
"AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".gif", ".webp" ],
"AllowedVideoExtensions": [ ".mp4", ".avi", ".mov", ".wmv" ],
"AllowedAudioExtensions": [ ".mp3", ".wav", ".ogg", ".m4a" ],
"AllowedDocumentExtensions": [ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt" ]
}
}

View File

@@ -1,62 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
//"MesbahDb": "Data Source=.\\MSSQLSERVER2019;Initial Catalog=mesbah_db;Persist Security Info=False;User ID=mesbah_db;Password=sa142857$@;"
"MesbahDb": "Data Source=.;Initial Catalog=mesbah_db;Integrated Security=True;TrustServerCertificate=true;",
//dad-mehr
//"MesbahDb": "Data Source=.;Initial Catalog=teamWork;Integrated Security=True;TrustServerCertificate=true;",
//testDb
"TestDb": "Data Source=.;Initial Catalog=TestDb;Integrated Security=True;TrustServerCertificate=true;",
//program_manager_db
"ProgramManagerDb": "Data Source=.;Initial Catalog=program_manager_db;Integrated Security=True;TrustServerCertificate=true;"
},
"BackupOptions": {
"DbName": "mesbah_db",
"DbBackupZipPath": "c://EveryHourBackupList//",
"FastDbBackupZipPath": "c://FastBackupZipList//",
"InsuranceListZipPath": "c://Logs//Gozareshgir//"
},
"faceModels": {
"Faces": "c://labels//20//"
},
"AllowedHosts": "*",
//"Domain": ".dad-mehr.ir"
"Domain": ".gozareshgir.ir",
"MongoDb": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "Gozareshgir"
},
"SmsSettings": {
"IsTestMode": false,
"TestNumbers": []
},
"InternalApi": {
"Local": "https://localhost:7032",
"Dadmehrg": "https://api.pm.dadmehrg.ir",
"Gozareshgir": "https://api.pm.gozareshgir.ir"
},
"SepehrGateWayTerminalId": 99213700,
"JwtSettings": {
"SecretKey": ">3£>^1UBG@yw)QdhRC3$£:;r8~?qpp^oKK4D3a~8L2>enF;lkgh",
"Issuer": "GozareshgirApp",
"Audience": "GozareshgirUsers",
"ExpirationMinutes": 30
},
"FileStorage": {
"BaseUrl": "https://api.pm.gozareshgir.ir/uploads",
"MaxFileSizeBytes": 104857600,
"AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".gif", ".webp" ],
"AllowedVideoExtensions": [ ".mp4", ".avi", ".mov", ".wmv" ],
"AllowedAudioExtensions": [ ".mp3", ".wav", ".ogg", ".m4a" ],
"AllowedDocumentExtensions": [ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt" ]
}
}