diff --git a/.gitignore b/.gitignore
index 674512f1..89d3b553 100644
--- a/.gitignore
+++ b/.gitignore
@@ -362,3 +362,9 @@ MigrationBackup/
# # Fody - auto-generated XML schema
# FodyWeavers.xsd
.idea
+/ServiceHost/appsettings.Development.json
+/ServiceHost/appsettings.json
+
+# Storage folder - ignore all uploaded files, thumbnails, and temporary files
+ServiceHost/Storage
+
diff --git a/Company.Domain/InstitutionContractSendFlagAgg/IInstitutionContractSendFlagRepository.cs b/Company.Domain/InstitutionContractSendFlagAgg/IInstitutionContractSendFlagRepository.cs
new file mode 100644
index 00000000..b847d2e0
--- /dev/null
+++ b/Company.Domain/InstitutionContractSendFlagAgg/IInstitutionContractSendFlagRepository.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Company.Domain.InstitutionContractSendFlagAgg;
+
+///
+/// Interface برای Repository مربوط به فلگ ارسال قرارداد
+///
+public interface IInstitutionContractSendFlagRepository
+{
+ ///
+ /// ایجاد یک رکورد جدید برای فلگ ارسال قرارداد
+ ///
+ Task Create(InstitutionContractSendFlag flag);
+
+ ///
+ /// بازیابی فلگ بر اساس شناسه قرارداد
+ ///
+ Task GetByContractId(long contractId);
+
+ ///
+ /// بهروزرسانی فلگ ارسال
+ ///
+ Task Update(InstitutionContractSendFlag flag);
+
+ ///
+ /// بررسی اینکه آیا قرارداد ارسال شده است
+ ///
+ Task IsContractSent(long contractId);
+
+
+}
+
diff --git a/Company.Domain/InstitutionContractSendFlagAgg/InstitutionContractSendFlag.cs b/Company.Domain/InstitutionContractSendFlagAgg/InstitutionContractSendFlag.cs
new file mode 100644
index 00000000..2b3a7137
--- /dev/null
+++ b/Company.Domain/InstitutionContractSendFlagAgg/InstitutionContractSendFlag.cs
@@ -0,0 +1,82 @@
+using System;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace Company.Domain.InstitutionContractSendFlagAgg;
+
+///
+/// نمایندگی فلگ ارسال قرارداد در MongoDB
+/// این موجودیت برای ردیابی اینکه آیا قرارداد ارسال شده است استفاده میشود
+///
+public class InstitutionContractSendFlag
+{
+ public InstitutionContractSendFlag(long institutionContractId,bool isSent)
+ {
+ Id = Guid.NewGuid();
+ InstitutionContractId = institutionContractId;
+ IsSent = isSent;
+ CreatedDate = DateTime.Now;
+ }
+
+ ///
+ /// شناسه یکتای MongoDB
+ ///
+ [BsonId]
+ [BsonRepresentation(BsonType.String)]
+ public Guid Id { get; set; }
+
+ ///
+ /// شناسه قرارداد در SQL
+ ///
+ public long InstitutionContractId { get; set; }
+
+ ///
+ /// آیا قرارداد ارسال شده است
+ ///
+ public bool IsSent { get; set; }
+
+ ///
+ /// تاریخ و زمان ارسال
+ ///
+ public DateTime? SentDate { get; set; }
+
+ ///
+ /// تاریخ و زمان ایجاد رکورد
+ ///
+ public DateTime CreatedDate { get; set; }
+
+ ///
+ /// تاریخ و زمان آخرین بهروزرسانی
+ ///
+ public DateTime? LastModifiedDate { get; set; }
+
+
+ ///
+ /// علامتگذاری قرارداد به عنوان ارسالشده
+ ///
+ public void MarkAsSent()
+ {
+ IsSent = true;
+ SentDate = DateTime.Now;
+ LastModifiedDate = DateTime.Now;
+ }
+
+ ///
+ /// بازگردانی علامت ارسال
+ ///
+ public void MarkAsNotSent()
+ {
+ IsSent = false;
+ SentDate = null;
+ LastModifiedDate = DateTime.Now;
+ }
+
+ ///
+ /// بهروزرسانی علامت آخری اصلاح
+ ///
+ public void UpdateLastModified()
+ {
+ LastModifiedDate = DateTime.Now;
+ }
+}
+
diff --git a/CompanyManagement.Infrastructure.Mongo/InstitutionContractSendFlagRepo/InstitutionContractSendFlagRepository.cs b/CompanyManagement.Infrastructure.Mongo/InstitutionContractSendFlagRepo/InstitutionContractSendFlagRepository.cs
new file mode 100644
index 00000000..ccc45260
--- /dev/null
+++ b/CompanyManagement.Infrastructure.Mongo/InstitutionContractSendFlagRepo/InstitutionContractSendFlagRepository.cs
@@ -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;
+
+///
+/// Repository برای مدیریت فلگ ارسال قرارداد در MongoDB
+///
+public class InstitutionContractSendFlagRepository : IInstitutionContractSendFlagRepository
+{
+ private readonly IMongoCollection _collection;
+
+ public InstitutionContractSendFlagRepository(IMongoDatabase database)
+ {
+ _collection = database.GetCollection("InstitutionContractSendFlag");
+ }
+
+ public async Task Create(InstitutionContractSendFlag flag)
+ {
+ await _collection.InsertOneAsync(flag);
+ }
+
+ public async Task GetByContractId(long contractId)
+ {
+ var filter = Builders.Filter
+ .Eq(x => x.InstitutionContractId, contractId);
+
+ return await _collection.Find(filter).FirstOrDefaultAsync();
+ }
+
+
+ public async Task Update(InstitutionContractSendFlag flag)
+ {
+ var filter = Builders.Filter
+ .Eq(x => x.InstitutionContractId, flag.InstitutionContractId);
+
+ await _collection.ReplaceOneAsync(filter, flag);
+ }
+
+ public async Task IsContractSent(long contractId)
+ {
+ var flag = await GetByContractId(contractId);
+ return flag != null && flag.IsSent;
+ }
+
+ public async Task Remove(long contractId)
+ {
+ var filter = Builders.Filter
+ .Eq(x => x.InstitutionContractId, contractId);
+
+ await _collection.DeleteOneAsync(filter);
+ }
+
+}
+
diff --git a/CompanyManagment.App.Contracts/InstitutionContract/GetInstitutionContractListItemsViewModel.cs b/CompanyManagment.App.Contracts/InstitutionContract/GetInstitutionContractListItemsViewModel.cs
index 7ba3aaa0..f69e1e75 100644
--- a/CompanyManagment.App.Contracts/InstitutionContract/GetInstitutionContractListItemsViewModel.cs
+++ b/CompanyManagment.App.Contracts/InstitutionContract/GetInstitutionContractListItemsViewModel.cs
@@ -96,6 +96,8 @@ public class GetInstitutionContractListItemsViewModel
/// مبلغ قسط
///
public double InstallmentAmount { get; set; }
+
+ public bool InstitutionContractIsSentFlag { get; set; }
}
public class InstitutionContractListWorkshop
diff --git a/CompanyManagment.App.Contracts/InstitutionContract/IInstitutionContractApplication.cs b/CompanyManagment.App.Contracts/InstitutionContract/IInstitutionContractApplication.cs
index 10266d6c..eb230aa4 100644
--- a/CompanyManagment.App.Contracts/InstitutionContract/IInstitutionContractApplication.cs
+++ b/CompanyManagment.App.Contracts/InstitutionContract/IInstitutionContractApplication.cs
@@ -148,7 +148,7 @@ public interface IInstitutionContractApplication
/// شناسه قرارداد
/// نتیجه عملیات
OperationResult UnSign(long id);
-
+
///
/// ایجاد حساب کاربری برای طرف قرارداد
///
@@ -305,6 +305,14 @@ public interface IInstitutionContractApplication
Task SetDiscountForCreation(InstitutionContractSetDiscountForCreationRequest request);
Task ResetDiscountForCreation(InstitutionContractResetDiscountForExtensionRequest request);
Task CreationComplete(InstitutionContractExtensionCompleteRequest request);
+
+ ///
+ /// تعیین فلگ ارسال قرارداد در MongoDB
+ /// اگر فلگ وجود نداشتند ایجاد میکند
+ ///
+ /// درخواست تعیین فلگ
+ /// نتیجه عملیات
+ Task SetContractSendFlag(SetInstitutionContractSendFlagRequest request);
}
public class CreationSetContractingPartyResponse
diff --git a/CompanyManagment.App.Contracts/InstitutionContract/SetInstitutionContractSendFlagRequest.cs b/CompanyManagment.App.Contracts/InstitutionContract/SetInstitutionContractSendFlagRequest.cs
new file mode 100644
index 00000000..e19bacf5
--- /dev/null
+++ b/CompanyManagment.App.Contracts/InstitutionContract/SetInstitutionContractSendFlagRequest.cs
@@ -0,0 +1,19 @@
+namespace CompanyManagment.App.Contracts.InstitutionContract;
+
+///
+/// درخواست برای تعیین فلگ ارسال قرارداد
+///
+public class SetInstitutionContractSendFlagRequest
+{
+ ///
+ /// شناسه قرارداد
+ ///
+ public long InstitutionContractId { get; set; }
+
+ ///
+ /// آیا قرارداد ارسال شده است
+ ///
+ public bool IsSent { get; set; }
+
+}
+
diff --git a/CompanyManagment.Application/InstitutionContractApplication.cs b/CompanyManagment.Application/InstitutionContractApplication.cs
index 5790aaee..199e6967 100644
--- a/CompanyManagment.Application/InstitutionContractApplication.cs
+++ b/CompanyManagment.Application/InstitutionContractApplication.cs
@@ -19,6 +19,7 @@ using Company.Domain.PaymentTransactionAgg;
using Company.Domain.RepresentativeAgg;
using Company.Domain.RollCallServiceAgg;
using Company.Domain.WorkshopAgg;
+using Company.Domain.InstitutionContractSendFlagAgg;
using CompanyManagment.App.Contracts.FinancialInvoice;
using CompanyManagment.App.Contracts.FinancialStatment;
using CompanyManagment.App.Contracts.InstitutionContract;
@@ -51,6 +52,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
private readonly IPaymentTransactionRepository _paymentTransactionRepository;
private readonly IRollCallServiceRepository _rollCallServiceRepository;
private readonly ISepehrPaymentGatewayService _sepehrPaymentGatewayService;
+ private readonly IInstitutionContractSendFlagRepository _institutionContractSendFlagRepository;
public InstitutionContractApplication(IInstitutionContractRepository institutionContractRepository,
@@ -62,7 +64,8 @@ public class InstitutionContractApplication : IInstitutionContractApplication
IAccountApplication accountApplication, ISmsService smsService,
IFinancialInvoiceRepository financialInvoiceRepository, IHttpClientFactory httpClientFactory,
IPaymentTransactionRepository paymentTransactionRepository, IRollCallServiceRepository rollCallServiceRepository,
- ISepehrPaymentGatewayService sepehrPaymentGatewayService,ILogger sepehrGatewayLogger)
+ ISepehrPaymentGatewayService sepehrPaymentGatewayService,ILogger sepehrGatewayLogger,
+ IInstitutionContractSendFlagRepository institutionContractSendFlagRepository)
{
_institutionContractRepository = institutionContractRepository;
_contractingPartyRepository = contractingPartyRepository;
@@ -80,6 +83,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
_rollCallServiceRepository = rollCallServiceRepository;
_sepehrPaymentGatewayService = sepehrPaymentGatewayService;
_paymentGateway = new SepehrPaymentGateway(httpClientFactory,sepehrGatewayLogger);
+ _institutionContractSendFlagRepository = institutionContractSendFlagRepository;
}
public OperationResult Create(CreateInstitutionContract command)
@@ -894,6 +898,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
return opration.Succcedded();
}
+
public void CreateContractingPartyAccount(long contractingPartyid, long accountId)
{
_institutionContractRepository.CreateContractingPartyAccount(contractingPartyid, accountId);
@@ -1820,7 +1825,60 @@ public class InstitutionContractApplication : IInstitutionContractApplication
installments.Add(lastInstallment);
return installments;
}
+
+
}
+ ///
+ /// تعیین فلگ ارسال قرارداد
+ /// اگر فلگ وجود نداشتند ایجاد میکند
+ ///
+ public async Task 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
diff --git a/CompanyManagment.Application/WorkshopAppliction.cs b/CompanyManagment.Application/WorkshopAppliction.cs
index a6efd5de..c2110263 100644
--- a/CompanyManagment.Application/WorkshopAppliction.cs
+++ b/CompanyManagment.Application/WorkshopAppliction.cs
@@ -407,6 +407,10 @@ public class WorkshopAppliction : IWorkshopApplication
public EditWorkshop GetDetails(long id)
{
var workshop = _workshopRepository.GetDetails(id);
+ if (workshop == null)
+ {
+ return null;
+ }
if (workshop.IsClassified)
{
workshop.CreatePlan = _workshopPlanApplication.GetWorkshopPlanByWorkshopId(id);
diff --git a/CompanyManagment.EFCore/Repository/InstitutionContractRepository.cs b/CompanyManagment.EFCore/Repository/InstitutionContractRepository.cs
index 44cd56e9..7f7f00d0 100644
--- a/CompanyManagment.EFCore/Repository/InstitutionContractRepository.cs
+++ b/CompanyManagment.EFCore/Repository/InstitutionContractRepository.cs
@@ -11,6 +11,7 @@ using Company.Domain.InstitutionContractAgg;
using Company.Domain.InstitutionContractAmendmentTempAgg;
using Company.Domain.InstitutionContractContactInfoAgg;
using Company.Domain.InstitutionContractExtensionTempAgg;
+using Company.Domain.InstitutionContractSendFlagAgg;
using Company.Domain.InstitutionPlanAgg;
using Company.Domain.SmsResultAgg;
using Company.Domain.WorkshopAgg;
@@ -42,6 +43,7 @@ using AccountManagement.Application.Contracts.Account;
using Company.Domain.InstitutionContractCreationTempAgg;
using Company.Domain.RepresentativeAgg;
using Company.Domain.TemporaryClientRegistrationAgg;
+using Company.Domain.InstitutionContractSendFlagAgg;
using ContractingPartyAccount = Company.Domain.ContractingPartyAccountAgg.ContractingPartyAccount;
using FinancialStatment = Company.Domain.FinancialStatmentAgg.FinancialStatment;
using String = System.String;
@@ -57,6 +59,7 @@ public class InstitutionContractRepository : RepositoryBase _institutionExtensionTemp;
private readonly IMongoCollection _institutionAmendmentTemp;
private readonly IMongoCollection _institutionContractCreationTemp;
+ private readonly IMongoCollection _institutionContractSendFlag;
private readonly IPlanPercentageRepository _planPercentageRepository;
private readonly ISmsService _smsService;
private readonly ISmsResultRepository _smsResultRepository;
@@ -114,6 +117,8 @@ public class InstitutionContractRepository : RepositoryBase("InstitutionContractAmendmentTemp");
_institutionContractCreationTemp =
database.GetCollection("InstitutionContractCreationTemp");
+ _institutionContractSendFlag =
+ database.GetCollection("InstitutionContractSendFlag");
}
public EditInstitutionContract GetDetails(long id)
@@ -1353,6 +1358,12 @@ public class InstitutionContractRepository : RepositoryBase contractIds.Contains(x.InstitutionContractId))
.ToDictionaryAsync(x => x.InstitutionContractId, x => x);
+ // بارگذاری وضعیت ارسال قراردادها از MongoDB - کوئری مستقیم
+ var filter = Builders.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)
.Where(x => contractingPartyIds.Contains(x.ContractingPartyId)).ToList();
var res = new PagedResult()
@@ -1462,7 +1473,8 @@ public class InstitutionContractRepository : RepositoryBase 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()
};
@@ -2002,7 +2014,7 @@ public class InstitutionContractRepository : RepositoryBase
- ///دریافت لیست پیامک قرادا های آبی بدهکار
+ ///دریافت لیست پیامک قراداد های آبی بدهکار
///
///
- //public async Task> GetBlueSmsListData()
+ //public async Task> GetWarningSmsListData()
//{
// var institutionContracts = await _context.InstitutionContractSet.AsQueryable().Select(x => new InstitutionContractViewModel
@@ -6358,6 +6370,8 @@ public class InstitutionContractRepository : RepositoryBase x.IsActiveString == "blue" &&
// 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 =>
// x.SigningType != InstitutionContractSigningType.Legacy && x.IsInstallment == false && x.SigningType != null).ToList();
@@ -6376,7 +6390,7 @@ public class InstitutionContractRepository : RepositoryBase institutionContracts.Select(ins => ins.Id).Contains(x.InstitutionContractId))
// .Where(x => x.SendSms && x.PhoneType == "شماره همراه" && !string.IsNullOrWhiteSpace(x.PhoneNumber) &&
// x.PhoneNumber.Length == 11).ToListAsync();
- // var legalActionSentSms =await _context.SmsResults
+ // var legalActionSentSms = await _context.SmsResults
// .Where(x => x.TypeOfSms == "اقدام قضایی").ToListAsync();
// var warningSentSms = await _context.SmsResults.Where(x => x.TypeOfSms.Contains("هشدار")).ToListAsync();
@@ -6389,8 +6403,13 @@ public class InstitutionContractRepository : RepositoryBase 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);
// var partyName = contractingParty.LName;
@@ -6425,7 +6444,7 @@ public class InstitutionContractRepository : RepositoryBase x.Deptor);
// var creditor = transactions.FinancialTransactionViewModels.Sum(x => x.Creditor);
-
+
// var id = $"{item.ContractingPartyId}";
// var aprove = $"{transactions.Id}";
@@ -6442,11 +6461,11 @@ public class InstitutionContractRepository : RepositoryBase (
// x.ContractingPatyId == contractingParty.Id &&
// x.Mobile == number.PhoneNumber) && (x.TypeOfSms == "اقدام قضایی" || x.TypeOfSms == "هشدار دوم"));
-
+ // var t = warningSentSms.Any(x=> x.)
// if (!string.IsNullOrWhiteSpace(number.PhoneNumber) &&
// number.PhoneNumber.Length == 11 && !isSend && !isLastAlarmSend)
// {
diff --git a/DadmehrGostar.sln b/DadmehrGostar.sln
index 342cf0ef..c39485ab 100644
--- a/DadmehrGostar.sln
+++ b/DadmehrGostar.sln
@@ -89,6 +89,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackgroundInstitutionContract.Task", "BackgroundInstitutionContract\BackgroundInstitutionContract.Task\BackgroundInstitutionContract.Task.csproj", "{F78FBB92-294B-88BA-168D-F0C578B0D7D6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProgramManager", "ProgramManager", "{67AFF7B6-4C4F-464C-A90D-9BDB644D83A9}"
+ ProjectSection(SolutionItems) = preProject
+ ProgramManager\appsettings.FileStorage.json = ProgramManager\appsettings.FileStorage.json
+ EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{48F6F6A5-7340-42F8-9216-BEB7A4B7D5A1}"
EndProject
diff --git a/PersonalContractingParty.Config/PersonalBootstrapper.cs b/PersonalContractingParty.Config/PersonalBootstrapper.cs
index 4765fafb..aab12f1f 100644
--- a/PersonalContractingParty.Config/PersonalBootstrapper.cs
+++ b/PersonalContractingParty.Config/PersonalBootstrapper.cs
@@ -61,6 +61,7 @@ using Company.Domain.HolidayItemAgg;
using Company.Domain.InstitutionContractAgg;
using Company.Domain.InstitutionContractContactInfoAgg;
using Company.Domain.InstitutionContractExtensionTempAgg;
+using Company.Domain.InstitutionContractSendFlagAgg;
using Company.Domain.InstitutionPlanAgg;
using Company.Domain.InsuranceAgg;
using Company.Domain.InsuranceEmployeeInfoAgg;
@@ -123,6 +124,7 @@ using Company.Domain.ZoneAgg;
using CompanyManagement.Infrastructure.Excel.SalaryAid;
using CompanyManagement.Infrastructure.Mongo.EmployeeFaceEmbeddingRepo;
using CompanyManagement.Infrastructure.Mongo.InstitutionContractInsertTempRepo;
+using CompanyManagement.Infrastructure.Mongo.InstitutionContractSendFlagRepo;
using CompanyManagment.App.Contracts.AdminMonthlyOverview;
using CompanyManagment.App.Contracts.AndroidApkVersion;
using CompanyManagment.App.Contracts.AuthorizedPerson;
@@ -658,6 +660,9 @@ public class PersonalBootstrapper
services.AddTransient();
services.AddTransient(); // MongoDB Implementation
+ // InstitutionContractSendFlag - MongoDB
+ services.AddTransient();
+
services.AddDbContext(x => x.UseSqlServer(connectionString));
}
}
\ No newline at end of file
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/GozareshgirProgramManager.Application.csproj b/ProgramManager/src/Application/GozareshgirProgramManager.Application/GozareshgirProgramManager.Application.csproj
index 41ec592d..d28d7b48 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/GozareshgirProgramManager.Application.csproj
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/GozareshgirProgramManager.Application.csproj
@@ -9,6 +9,7 @@
+
@@ -18,4 +19,10 @@
+
+
+ C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.AspNetCore.Http.Features.dll
+
+
+
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AddTaskToPhase/AddTaskToPhaseCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AddTaskToPhase/AddTaskToPhaseCommand.cs
index ba82f57c..1373f8a6 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AddTaskToPhase/AddTaskToPhaseCommand.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AddTaskToPhase/AddTaskToPhaseCommand.cs
@@ -10,7 +10,7 @@ public record AddTaskToPhaseCommand(
Guid PhaseId,
string Name,
string? Description = null,
- TaskPriority Priority = TaskPriority.Medium,
+ ProjectTaskPriority Priority = ProjectTaskPriority.Medium,
int OrderIndex = 0,
DateTime? DueDate = null
) : IBaseCommand;
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommand.cs
new file mode 100644
index 00000000..4b21408b
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommand.cs
@@ -0,0 +1,57 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Domain._Common;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
+using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
+
+namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion;
+
+public record ApproveTaskSectionCompletionCommand(Guid TaskSectionId, bool IsApproved) : IBaseCommand;
+
+public class ApproveTaskSectionCompletionCommandHandler : IBaseCommandHandler
+{
+ private readonly ITaskSectionRepository _taskSectionRepository;
+ private readonly IUnitOfWork _unitOfWork;
+ private readonly IAuthHelper _authHelper;
+
+ public ApproveTaskSectionCompletionCommandHandler(
+ ITaskSectionRepository taskSectionRepository,
+ IUnitOfWork unitOfWork,
+ IAuthHelper authHelper)
+ {
+ _taskSectionRepository = taskSectionRepository;
+ _unitOfWork = unitOfWork;
+ _authHelper = authHelper;
+ }
+
+ public async Task Handle(ApproveTaskSectionCompletionCommand request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId()
+ ?? throw new UnAuthorizedException(" ? ");
+
+ var section = await _taskSectionRepository.GetByIdAsync(request.TaskSectionId, cancellationToken);
+ if (section == null)
+ {
+ return OperationResult.NotFound(" ? ");
+ }
+
+ if (section.Status != TaskSectionStatus.PendingForCompletion)
+ {
+ return OperationResult.Failure(" ԝ?? ʘ? ?? ? ");
+ }
+
+ if (request.IsApproved)
+ {
+ section.UpdateStatus(TaskSectionStatus.Completed);
+ }
+ else
+ {
+ section.UpdateStatus(TaskSectionStatus.Incomplete);
+ }
+
+ await _unitOfWork.SaveChangesAsync(cancellationToken);
+
+ return OperationResult.Success();
+ }
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommandValidator.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommandValidator.cs
new file mode 100644
index 00000000..c12a43c3
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ApproveTaskSectionCompletion/ApproveTaskSectionCompletionCommandValidator.cs
@@ -0,0 +1,14 @@
+using FluentValidation;
+
+namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion;
+
+public class ApproveTaskSectionCompletionCommandValidator : AbstractValidator
+{
+ public ApproveTaskSectionCompletionCommandValidator()
+ {
+ RuleFor(c => c.TaskSectionId)
+ .NotEmpty()
+ .NotNull()
+ .WithMessage(" ? ? ");
+ }
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AutoPendingFullTimeTaskSections/AutoPendingFullTimeTaskSectionsCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AutoPendingFullTimeTaskSections/AutoPendingFullTimeTaskSectionsCommand.cs
new file mode 100644
index 00000000..6e1e9802
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AutoPendingFullTimeTaskSections/AutoPendingFullTimeTaskSectionsCommand.cs
@@ -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
+{
+ private readonly ITaskSectionRepository _taskSectionRepository;
+ private readonly IUnitOfWork _unitOfWork;
+
+ public AutoPendingFullTimeTaskSectionsCommandHandler(
+ ITaskSectionRepository taskSectionRepository,
+ IUnitOfWork unitOfWork)
+ {
+ _taskSectionRepository = taskSectionRepository;
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task 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}");
+ }
+ }
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs
index 9e6803f5..178a0257 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs
@@ -52,7 +52,10 @@ public class ChangeStatusSectionCommandHandler : IBaseCommandHandler>
{
{ TaskSectionStatus.ReadyToStart, [TaskSectionStatus.InProgress] },
- { TaskSectionStatus.InProgress, [TaskSectionStatus.Incomplete, TaskSectionStatus.Completed] },
- { TaskSectionStatus.Incomplete, [TaskSectionStatus.InProgress, TaskSectionStatus.Completed] },
- { TaskSectionStatus.Completed, [TaskSectionStatus.InProgress, TaskSectionStatus.Incomplete] }, // Can return to InProgress or Incomplete
+ { TaskSectionStatus.InProgress, [TaskSectionStatus.Incomplete, TaskSectionStatus.PendingForCompletion] },
+ { TaskSectionStatus.Incomplete, [TaskSectionStatus.InProgress, TaskSectionStatus.PendingForCompletion] },
+ { TaskSectionStatus.PendingForCompletion, [TaskSectionStatus.InProgress, TaskSectionStatus.Incomplete] }, // Can return to InProgress or Incomplete
{ TaskSectionStatus.NotAssigned, [TaskSectionStatus.InProgress, TaskSectionStatus.ReadyToStart] }
};
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeTaskPriority/ChangeTaskPriorityCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeTaskPriority/ChangeTaskPriorityCommand.cs
new file mode 100644
index 00000000..d6ea7e1e
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeTaskPriority/ChangeTaskPriorityCommand.cs
@@ -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
+{
+ 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 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 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 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();
+ 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 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();
+ foreach (var phase in phases)
+ {
+ var tasks = phase.Tasks?.ToList() ?? new List();
+ foreach (var t in tasks)
+ {
+ if (t.Priority != priority)
+ {
+ t.SetPriority(priority);
+ }
+ }
+ }
+
+ await _unitOfWork.SaveChangesAsync(ct);
+ return OperationResult.Success();
+ }
+}
+
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommand.cs
index 230754b5..b7ed7859 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommand.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommand.cs
@@ -4,10 +4,15 @@ using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimeProject;
-public record SetTimeProjectCommand(List SectionItems, Guid Id, ProjectHierarchyLevel Level):IBaseCommand;
+public record SetTimeProjectCommand(
+ List SkillItems,
+ Guid Id,
+ ProjectHierarchyLevel Level,
+ bool CascadeToChildren) : IBaseCommand;
public class SetTimeSectionTime
{
public string Description { get; set; }
public int Hours { get; set; }
+ public int Minutes { get; set; }
}
\ No newline at end of file
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommandHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommandHandler.cs
index f6e9bafa..a9ca3a61 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommandHandler.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/SetTimeProject/SetTimeProjectCommandHandler.cs
@@ -6,6 +6,8 @@ using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
+using GozareshgirProgramManager.Domain.SkillAgg.Repositories;
+using GozareshgirProgramManager.Domain.UserAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimeProject;
@@ -15,21 +17,33 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler SetTimeForProject(SetTimeProjectCommand request,
- CancellationToken cancellationToken)
+ private async Task AssignProject(SetTimeProjectCommand request)
{
var project = await _projectRepository.GetWithFullHierarchyAsync(request.Id);
- if (project == null)
+ if (project is null)
{
return OperationResult.NotFound("پروژه یافت نشد");
- return OperationResult.NotFound("���� ���� ���");
}
- long? addedByUserId = _userId;
+ var skillItems = request.SkillItems.Where(x=>x.UserId is > 0).ToList();
+
+ // حذف ProjectSections که در validSkills نیستند
+ var validSkillIds = skillItems.Select(x => x.SkillId).ToList();
+ var sectionsToRemove = project.ProjectSections
+ .Where(s => !validSkillIds.Contains(s.SkillId))
+ .ToList();
+
+ foreach (var section in sectionsToRemove)
+ {
+ project.RemoveProjectSection(section.SkillId);
+ }
+
+ // تخصیص در سطح پروژه
+ foreach (var item in skillItems)
+ {
+ var skill = await _skillRepository.GetByIdAsync(item.SkillId);
+ if (skill is null)
+ {
+ return OperationResult.NotFound($"مهارت با شناسه {item.SkillId} یافت نشد");
+ }
- // تنظیم زمان برای تمام sections در تمام فازها و تسکهای پروژه
+ // بررسی و بهروزرسانی یا اضافه کردن ProjectSection
+ var existingSection = project.ProjectSections.FirstOrDefault(s => s.SkillId == item.SkillId);
+ if (existingSection != null)
+ {
+ // اگر وجود داشت، فقط userId را بهروزرسانی کن
+ existingSection.UpdateUser(item.UserId.Value);
+ }
+ else
+ {
+ // اگر وجود نداشت، اضافه کن
+ var newSection = new ProjectSection(project.Id, item.UserId.Value, item.SkillId);
+ await _projectSectionRepository.CreateAsync(newSection);
+ }
+ }
+
+ // حالا برای تمام فازها و تسکها cascade کن
foreach (var phase in project.Phases)
{
- foreach (var task in phase.Tasks)
+ // اگر CascadeToChildren true است یا فاز override ندارد
+ if (request.CascadeToChildren || !phase.HasAssignmentOverride)
{
- foreach (var section in task.Sections)
+ // حذف PhaseSections که در validSkills نیستند
+ var phaseSectionsToRemove = phase.PhaseSections
+ .Where(s => !validSkillIds.Contains(s.SkillId))
+ .ToList();
+
+ foreach (var section in phaseSectionsToRemove)
{
- var sectionItem = request.SectionItems.FirstOrDefault(si => si.SectionId == section.Id);
- if (sectionItem != null)
+ phase.RemovePhaseSection(section.SkillId);
+ }
+
+ // برای phase هم باید sectionها را بهروزرسانی کنیم
+ foreach (var item in skillItems )
+ {
+ var existingSection = phase.PhaseSections.FirstOrDefault(s => s.SkillId == item.SkillId);
+ if (existingSection != null)
{
- SetSectionTime(section, sectionItem, addedByUserId);
+ existingSection.Update(item.UserId.Value, item.SkillId);
+ }
+ else
+ {
+ var newPhaseSection = new PhaseSection(phase.Id, item.UserId.Value, item.SkillId);
+ await _phaseSectionRepository.CreateAsync(newPhaseSection);
+ }
+ }
+
+ foreach (var task in phase.Tasks)
+ {
+ // اگر CascadeToChildren true است یا تسک override ندارد
+ if (request.CascadeToChildren || !task.HasAssignmentOverride)
+ {
+ // حذف TaskSections که در validSkills نیستند
+ var taskSectionsToRemove = task.Sections
+ .Where(s => !validSkillIds.Contains(s.SkillId))
+ .ToList();
+
+ foreach (var section in taskSectionsToRemove)
+ {
+ task.RemoveSection(section.Id);
+ }
+
+ foreach (var item in skillItems)
+ {
+ var section = task.Sections.FirstOrDefault(s => s.SkillId == item.SkillId);
+ if (section != null)
+ {
+ // استفاده از TransferToUser
+ if (section.CurrentAssignedUserId != item.UserId)
+ {
+ if (section.CurrentAssignedUserId > 0)
+ {
+ section.TransferToUser(section.CurrentAssignedUserId, item.UserId.Value);
+ }
+ else
+ {
+ section.AssignToUser(item.UserId.Value);
+ }
+ }
+ }
+ else
+ {
+ var newTaskSection = new TaskSection(task.Id, item.SkillId, item.UserId.Value);
+ await _taskSectionRepository.CreateAsync(newTaskSection);
+ }
+ }
}
}
}
}
- await _unitOfWork.SaveChangesAsync(cancellationToken);
+ await _unitOfWork.SaveChangesAsync();
return OperationResult.Success();
}
- private async Task SetTimeForProjectPhase(SetTimeProjectCommand request,
- CancellationToken cancellationToken)
+ private async Task AssignProjectPhase(SetTimeProjectCommand request)
{
var phase = await _projectPhaseRepository.GetWithTasksAsync(request.Id);
- if (phase == null)
+ if (phase is null)
{
return OperationResult.NotFound("فاز پروژه یافت نشد");
- return OperationResult.NotFound("��� ���� ���� ���");
}
- long? addedByUserId = _userId;
+ // تخصیص در سطح فاز
+ foreach (var item in request.SkillItems)
+ {
+ var skill = await _skillRepository.GetByIdAsync(item.SkillId);
+ if (skill is null)
+ {
+ return OperationResult.NotFound($"مهارت با شناسه {item.SkillId} یافت نشد");
+ }
+ }
- // تنظیم زمان برای تمام sections در تمام تسکهای این فاز
+ // علامتگذاری که این فاز نسبت به parent متمایز است
+ phase.MarkAsOverridden();
+
+ var skillItems = request.SkillItems.Where(x=>x.UserId is > 0).ToList();
+
+ // حذف PhaseSections که در validSkills نیستند
+ var validSkillIds = skillItems.Select(x => x.SkillId).ToList();
+ var sectionsToRemove = phase.PhaseSections
+ .Where(s => !validSkillIds.Contains(s.SkillId))
+ .ToList();
+
+ foreach (var section in sectionsToRemove)
+ {
+ phase.RemovePhaseSection(section.SkillId);
+ }
+
+ // بهروزرسانی یا اضافه کردن PhaseSection
+ foreach (var item in skillItems)
+ {
+ var existingSection = phase.PhaseSections.FirstOrDefault(s => s.SkillId == item.SkillId);
+ if (existingSection != null)
+ {
+ // اگر وجود داشت، فقط userId را بهروزرسانی کن
+ existingSection.Update(item.UserId!.Value, item.SkillId);
+ }
+ else
+ {
+ // اگر وجود نداشت، اضافه کن
+ var newPhaseSection = new PhaseSection(phase.Id, item.UserId!.Value, item.SkillId);
+ await _phaseSectionRepository.CreateAsync(newPhaseSection);
+ }
+ }
+
+ // cascade به تمام تسکها
foreach (var task in phase.Tasks)
{
- foreach (var section in task.Sections)
+ // اگر CascadeToChildren true است یا تسک override ندارد
+ if (request.CascadeToChildren || !task.HasAssignmentOverride)
{
- var sectionItem = request.SectionItems.FirstOrDefault(si => si.SectionId == section.Id);
- if (sectionItem != null)
+ // حذف TaskSections که در validSkills نیستند
+ var taskSectionsToRemove = task.Sections
+ .Where(s => !validSkillIds.Contains(s.SkillId))
+ .ToList();
+
+ foreach (var section in taskSectionsToRemove)
{
- SetSectionTime(section, sectionItem, addedByUserId);
+ task.RemoveSection(section.Id);
+ }
+
+ foreach (var item in skillItems)
+ {
+ var section = task.Sections.FirstOrDefault(s => s.SkillId == item.SkillId);
+ if (section != null)
+ {
+ // استفاده از TransferToUser
+ if (section.CurrentAssignedUserId != item.UserId)
+ {
+ if (section.CurrentAssignedUserId > 0)
+ {
+ section.TransferToUser(section.CurrentAssignedUserId, item.UserId!.Value);
+ }
+ else
+ {
+ section.AssignToUser(item.UserId!.Value);
+ }
+ }
+ }
+ else
+ {
+ var newTaskSection = new TaskSection(task.Id, item.SkillId, item.UserId!.Value);
+ await _taskSectionRepository.CreateAsync(newTaskSection);
+ }
}
}
}
- await _unitOfWork.SaveChangesAsync(cancellationToken);
+ await _unitOfWork.SaveChangesAsync();
return OperationResult.Success();
}
+
private async Task SetTimeForProjectTask(SetTimeProjectCommand request,
CancellationToken cancellationToken)
{
@@ -116,24 +296,64 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandlerx.UserId is > 0).ToList();
+
+ // حذف سکشنهایی که در validSkills نیستند
+ var validSkillIds = validSkills.Select(x => x.SkillId).ToList();
+ var sectionsToRemove = task.Sections
+ .Where(s => !validSkillIds.Contains(s.SkillId))
+ .ToList();
+
+ foreach (var sectionToRemove in sectionsToRemove)
{
- var sectionItem = request.SectionItems.FirstOrDefault(si => si.SectionId == section.Id);
- if (sectionItem != null)
- {
- SetSectionTime(section, sectionItem, addedByUserId);
- }
+ task.RemoveSection(sectionToRemove.Id);
}
+
+ foreach (var skillItem in validSkills)
+ {
+ var section = task.Sections.FirstOrDefault(s => s.SkillId == skillItem.SkillId);
+ if (!_userRepository.Exists(x=>x.Id == skillItem.UserId!.Value))
+ {
+ throw new BadRequestException("کاربر با شناسه یافت نشد.");
+ }
+
+ if (section == null)
+ {
+ var taskSection = new TaskSection(task.Id,
+ skillItem.SkillId, skillItem.UserId!.Value);
+
+ task.AddSection(taskSection);
+ section = taskSection;
+ }
+ else
+ {
+ if (section.CurrentAssignedUserId != skillItem.UserId)
+ {
+ if (section.CurrentAssignedUserId > 0)
+ {
+ section.TransferToUser(section.CurrentAssignedUserId, skillItem.UserId!.Value);
+ }
+ else
+ {
+ section.AssignToUser(skillItem.UserId!.Value);
+ }
+ }
+ }
+
+ SetSectionTime(section, skillItem, addedByUserId);
+
+ }
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
- private void SetSectionTime(TaskSection section, SetTimeProjectSectionItem sectionItem, long? addedByUserId)
+ private void SetSectionTime(TaskSection section, SetTimeProjectSkillItem sectionItem, long? addedByUserId)
{
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)
{
@@ -145,10 +365,26 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler x.SectionItems)
- .SetValidator(command => new SetTimeProjectSectionItemValidator());
-
- RuleFor(x => x.SectionItems)
- .Must(sectionItems => sectionItems.Any(si => si.InitData?.Hours > 0))
- .WithMessage("حداقل یکی از بخشها باید مقدار ساعت معتبری داشته باشد.");
+ RuleForEach(x => x.SkillItems)
+ .SetValidator(command => new SetTimeProjectSkillItemValidator());
}
}
-public class SetTimeProjectSectionItemValidator:AbstractValidator
+public class SetTimeProjectSkillItemValidator:AbstractValidator
{
- public SetTimeProjectSectionItemValidator()
+ public SetTimeProjectSkillItemValidator()
{
- RuleFor(x=>x.SectionId)
+ RuleFor(x=>x.SkillId)
.NotEmpty()
.NotNull()
.WithMessage("شناسه بخش نمیتواند خالی باشد.");
@@ -47,6 +43,18 @@ public class AdditionalTimeDataValidator: AbstractValidator
.GreaterThanOrEqualTo(0)
.WithMessage("ساعت نمیتواند منفی باشد.");
+ RuleFor(x => x.Hours)
+ .LessThan(1_000)
+ .WithMessage("ساعت باید کمتر از 1000 باشد.");
+
+ RuleFor(x => x.Minutes)
+ .GreaterThanOrEqualTo(0)
+ .WithMessage("دقیقه نمیتواند منفی باشد.");
+
+ RuleFor(x => x.Minutes)
+ .LessThan(60)
+ .WithMessage("دقیقه باید بین 0 تا 59 باشد.");
+
RuleFor(x=>x.Description)
.MaximumLength(500)
.WithMessage("توضیحات نمیتواند بیشتر از 500 کاراکتر باشد.");
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/DTOs/ProjectHierarchyDtos.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/DTOs/ProjectHierarchyDtos.cs
index 3cbc8cec..4a3d9294 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/DTOs/ProjectHierarchyDtos.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/DTOs/ProjectHierarchyDtos.cs
@@ -89,6 +89,7 @@ public class ProjectSectionDto
public TimeSpan FinalEstimatedHours { get; set; }
public TimeSpan TotalTimeSpent { get; set; }
+ public double ProgressPercentage { get; set; }
public bool IsCompleted { get; set; }
public bool IsInProgress { get; set; }
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/DTOs/SetTimeProjectSectionItem.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/DTOs/SetTimeProjectSectionItem.cs
index 04566e4e..75fdaaf8 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/DTOs/SetTimeProjectSectionItem.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/DTOs/SetTimeProjectSectionItem.cs
@@ -2,9 +2,10 @@ using GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimePro
namespace GozareshgirProgramManager.Application.Modules.Projects.DTOs;
-public class SetTimeProjectSectionItem
+public class SetTimeProjectSkillItem
{
- public Guid SectionId { get; set; }
+ public Guid SkillId { get; set; }
+ public long? UserId { get; set; }
public SetTimeSectionTime InitData { get; set; }
public List AdditionalTime { get; set; } = [];
}
\ No newline at end of file
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Extensions/ProjectMappingExtensions.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Extensions/ProjectMappingExtensions.cs
index 67ce006e..2c689cad 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Extensions/ProjectMappingExtensions.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Extensions/ProjectMappingExtensions.cs
@@ -166,6 +166,7 @@ public static class ProjectMappingExtensions
CreationDate = section.CreationDate,
FinalEstimatedHours = section.FinalEstimatedHours,
TotalTimeSpent = section.GetTotalTimeSpent(),
+ ProgressPercentage = section.GetProgressPercentage(),
IsCompleted = section.IsCompleted(),
IsInProgress = section.IsInProgress(),
Activities = section.Activities.Select(a => a.ToDto()).ToList(),
@@ -188,6 +189,7 @@ public static class ProjectMappingExtensions
CreationDate = section.CreationDate,
FinalEstimatedHours = section.FinalEstimatedHours,
TotalTimeSpent = section.GetTotalTimeSpent(),
+ ProgressPercentage = section.GetProgressPercentage(),
IsCompleted = section.IsCompleted(),
IsInProgress = section.IsInProgress()
// No activities or additional times for summary
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQuery.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQuery.cs
new file mode 100644
index 00000000..8e9e292d
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQuery.cs
@@ -0,0 +1,11 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+
+namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
+
+///
+/// درخواست جستجو در سراسر سلسلهمراتب پروژه (پروژه، فاز، تسک).
+/// نتایج با اطلاعات مسیر سلسلهمراتب برای پشتیبانی از ناوبری درخت در رابط کاربری بازگردانده میشود.
+///
+public record GetProjectSearchQuery(
+ string SearchQuery) : IBaseQuery;
+
\ No newline at end of file
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQueryHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQueryHandler.cs
new file mode 100644
index 00000000..e9cb3c25
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQueryHandler.cs
@@ -0,0 +1,132 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
+using Microsoft.EntityFrameworkCore;
+
+namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
+
+///
+/// Handler برای درخواست جستجوی سراسری در سلسلهمراتب پروژه.
+/// این handler در تمام سطحهای پروژه، فاز و تسک جستجو میکند و از تمام فیلدهای متنی (نام، توضیحات) استفاده میکند.
+/// همچنین در زیرمجموعههای هر سطح (ProjectSections، PhaseSections، TaskSections) جستجو میکند.
+///
+public class GetProjectSearchQueryHandler : IBaseQueryHandler
+{
+ private readonly IProgramManagerDbContext _context;
+ private const int MaxResults = 50;
+
+ public GetProjectSearchQueryHandler(IProgramManagerDbContext context)
+ {
+ _context = context;
+ }
+
+ public async Task> Handle(
+ GetProjectSearchQuery request,
+ CancellationToken cancellationToken)
+ {
+ var searchQuery = request.SearchQuery.ToLower();
+ var results = new List();
+
+ // جستجو در پروژهها و ProjectSections
+ var projects = await SearchProjects(searchQuery, cancellationToken);
+ results.AddRange(projects);
+
+ // جستجو در فازها و PhaseSections
+ var phases = await SearchPhases(searchQuery, cancellationToken);
+ results.AddRange(phases);
+
+ // جستجو در تسکها و TaskSections
+ var tasks = await SearchTasks(searchQuery, cancellationToken);
+ results.AddRange(tasks);
+
+ // مرتبسازی نتایج: ابتدا بر اساس سطح سلسلهمراتب (پروژه → فاز → تسک)، سپس بر اساس نام
+ var sortedResults = results
+ .OrderBy(r => r.Level)
+ .ThenBy(r => r.Title)
+ .Take(MaxResults)
+ .ToList();
+
+ var response = new GetProjectSearchResponse(sortedResults);
+ return OperationResult.Success(response);
+ }
+
+ ///
+ /// جستجو در جدول پروژهها (نام، توضیحات) و ProjectSections (نام مهارت، توضیحات اولیه)
+ ///
+ private async Task> SearchProjects(
+ string searchQuery,
+ CancellationToken cancellationToken)
+ {
+ var projects = await _context.Projects
+ .Where(p =>
+ p.Name.ToLower().Contains(searchQuery) ||
+ (p.Description != null && p.Description.ToLower().Contains(searchQuery)))
+ .Select(p => new ProjectHierarchySearchResultDto
+ {
+ Id = p.Id,
+ Title = p.Name,
+ Level = ProjectHierarchyLevel.Project,
+ ProjectId = null,
+ PhaseId = null
+ })
+ .ToListAsync(cancellationToken);
+
+ return projects;
+ }
+
+ ///
+ /// جستجو در جدول فازهای پروژه (نام، توضیحات) و PhaseSections
+ ///
+ private async Task> SearchPhases(
+ string searchQuery,
+ CancellationToken cancellationToken)
+ {
+ var phases = await _context.ProjectPhases
+ .Where(ph =>
+ ph.Name.ToLower().Contains(searchQuery) ||
+ (ph.Description != null && ph.Description.ToLower().Contains(searchQuery)))
+ .Select(ph => new ProjectHierarchySearchResultDto
+ {
+ Id = ph.Id,
+ Title = ph.Name,
+ Level = ProjectHierarchyLevel.Phase,
+ ProjectId = ph.ProjectId,
+ PhaseId = null
+ })
+ .ToListAsync(cancellationToken);
+
+ return phases;
+ }
+
+ ///
+ /// جستجو در جدول تسکهای پروژه (نام، توضیحات) و TaskSections (نام مهارت، توضیح اولیه، اطلاعات اضافی)
+ ///
+ private async Task> SearchTasks(
+ string searchQuery,
+ CancellationToken cancellationToken)
+ {
+ var tasks = await _context.ProjectTasks
+ .Include(t => t.Sections)
+ .Include(t => t.Phase)
+ .Where(t =>
+ t.Name.ToLower().Contains(searchQuery) ||
+ (t.Description != null && t.Description.ToLower().Contains(searchQuery)) ||
+ t.Sections.Any(s =>
+ (s.InitialDescription != null && s.InitialDescription.ToLower().Contains(searchQuery)) ||
+ s.AdditionalTimes.Any(at => at.Reason != null && at.Reason.ToLower().Contains(searchQuery))))
+ .Select(t => new ProjectHierarchySearchResultDto
+ {
+ Id = t.Id,
+ Title = t.Name,
+ Level = ProjectHierarchyLevel.Task,
+ ProjectId = t.Phase.ProjectId,
+ PhaseId = t.PhaseId
+ })
+ .ToListAsync(cancellationToken);
+
+ return tasks;
+ }
+
+}
+
+
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQueryValidator.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQueryValidator.cs
new file mode 100644
index 00000000..70c25241
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchQueryValidator.cs
@@ -0,0 +1,18 @@
+using FluentValidation;
+
+namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
+
+///
+/// اعتبارسنج برای درخواست جستجوی سراسری
+///
+public class GetProjectSearchQueryValidator : AbstractValidator
+{
+ public GetProjectSearchQueryValidator()
+ {
+ RuleFor(x => x.SearchQuery)
+ .NotEmpty().WithMessage("متن جستجو نمیتواند خالی باشد.")
+ .MinimumLength(2).WithMessage("متن جستجو باید حداقل 2 حرف باشد.")
+ .MaximumLength(500).WithMessage("متن جستجو نمیتواند بیش از 500 حرف باشد.");
+ }
+}
+
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchResponse.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchResponse.cs
new file mode 100644
index 00000000..288206f7
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/GetProjectSearchResponse.cs
@@ -0,0 +1,8 @@
+namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
+
+///
+/// پوستهی پاسخ برای نتایج جستجوی سراسری
+///
+public record GetProjectSearchResponse(
+ List Results);
+
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/ProjectHierarchySearchResultDto.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/ProjectHierarchySearchResultDto.cs
new file mode 100644
index 00000000..036d3a8b
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectHierarchySearch/ProjectHierarchySearchResultDto.cs
@@ -0,0 +1,36 @@
+using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
+
+namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
+
+///
+/// DTO برای نتایج جستجوی سراسری در سلسلهمراتب پروژه.
+/// حاوی اطلاعات کافی برای بازسازی مسیر سلسلهمراتب و بسط درخت در رابط کاربری است.
+///
+public record ProjectHierarchySearchResultDto
+{
+ ///
+ /// شناسه آیتم (پروژه، فاز یا تسک)
+ ///
+ public Guid Id { get; init; }
+
+ ///
+ /// نام/عنوان آیتم
+ ///
+ public string Title { get; init; } = string.Empty;
+
+ ///
+ /// سطح سلسلهمراتب این آیتم
+ ///
+ public ProjectHierarchyLevel Level { get; init; }
+
+ ///
+ /// شناسه پروژه - همیشه برای فاز و تسک پر شده است، برای پروژه با شناسه خود پر میشود
+ ///
+ public Guid? ProjectId { get; init; }
+
+ ///
+ /// شناسه فاز - فقط برای تسک پر شده است، برای پروژه و فاز خالی است
+ ///
+ public Guid? PhaseId { get; init; }
+}
+
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectListDto.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectListDto.cs
index 74a58946..91a82c28 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectListDto.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectListDto.cs
@@ -1,18 +1,28 @@
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
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 string Name { get; init; } = string.Empty;
public int Percentage { get; init; }
public ProjectHierarchyLevel Level { 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 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
+{
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListQueryHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListQueryHandler.cs
index 1815c2bc..a982caf3 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListQueryHandler.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListQueryHandler.cs
@@ -3,6 +3,7 @@ using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using Microsoft.EntityFrameworkCore;
+using System.Linq;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
@@ -17,278 +18,343 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler> Handle(GetProjectsListQuery request, CancellationToken cancellationToken)
{
- List projects;
+ var projects = new List();
+ var phases = new List();
+ var tasks = new List();
switch (request.HierarchyLevel)
{
case ProjectHierarchyLevel.Project:
projects = await GetProjects(request.ParentId, cancellationToken);
+ await SetSkillFlags(projects, cancellationToken);
break;
case ProjectHierarchyLevel.Phase:
- projects = await GetPhases(request.ParentId, cancellationToken);
+ phases = await GetPhases(request.ParentId, cancellationToken);
+ await SetSkillFlags(phases, cancellationToken);
break;
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;
default:
return OperationResult.Failure("سطح سلسله مراتب نامعتبر است");
}
- await SetSkillFlags(projects, cancellationToken);
- var response = new GetProjectsListResponse(projects);
+ var response = new GetProjectsListResponse(projects, phases, tasks);
return OperationResult.Success(response);
}
- private async Task> GetProjects(Guid? parentId, CancellationToken cancellationToken)
+ private async Task> GetProjects(Guid? parentId, CancellationToken cancellationToken)
{
var query = _context.Projects.AsQueryable();
-
- // پروژهها سطح بالا هستند و parentId ندارند، فقط در صورت null بودن parentId نمایش داده میشوند
if (parentId.HasValue)
{
- return new List(); // پروژهها parent ندارند
+ return new List();
}
-
- var projects = await query
+ var entities = await query
.OrderByDescending(p => p.CreationDate)
.ToListAsync(cancellationToken);
- var result = new List();
-
- foreach (var project in projects)
+ var result = new List();
+ foreach (var project in entities)
{
- var percentage = await CalculateProjectPercentage(project, cancellationToken);
- result.Add(new GetProjectListDto
+ var (percentage, totalTime) = await CalculateProjectPercentage(project, cancellationToken);
+ result.Add(new GetProjectDto
{
Id = project.Id,
Name = project.Name,
Level = ProjectHierarchyLevel.Project,
ParentId = null,
- Percentage = percentage
+ Percentage = percentage,
+ TotalHours = (int)totalTime.TotalHours,
+ Minutes = totalTime.Minutes,
});
}
-
return result;
}
- private async Task> GetPhases(Guid? parentId, CancellationToken cancellationToken)
+ private async Task> GetPhases(Guid? parentId, CancellationToken cancellationToken)
{
var query = _context.ProjectPhases.AsQueryable();
-
if (parentId.HasValue)
{
query = query.Where(x => x.ProjectId == parentId);
}
-
- var phases = await query
+ var entities = await query
.OrderByDescending(p => p.CreationDate)
.ToListAsync(cancellationToken);
- var result = new List();
-
- foreach (var phase in phases)
+ var result = new List();
+ foreach (var phase in entities)
{
- var percentage = await CalculatePhasePercentage(phase, cancellationToken);
- result.Add(new GetProjectListDto
+ var (percentage, totalTime) = await CalculatePhasePercentage(phase, cancellationToken);
+ result.Add(new GetPhaseDto
{
Id = phase.Id,
Name = phase.Name,
Level = ProjectHierarchyLevel.Phase,
ParentId = phase.ProjectId,
- Percentage = percentage
+ Percentage = percentage,
+ TotalHours = (int)totalTime.TotalHours,
+ Minutes = totalTime.Minutes,
});
}
-
return result;
}
- private async Task> GetTasks(Guid? parentId, CancellationToken cancellationToken)
+ private async Task> GetTasks(Guid? parentId, CancellationToken cancellationToken)
{
var query = _context.ProjectTasks.AsQueryable();
-
if (parentId.HasValue)
{
query = query.Where(x => x.PhaseId == parentId);
}
-
- var tasks = await query
+ var entities = await query
.OrderByDescending(t => t.CreationDate)
.ToListAsync(cancellationToken);
- var result = new List();
+ var result = new List();
+ // دریافت تمام 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 = await CalculateTaskPercentage(task, cancellationToken);
- result.Add(new GetProjectListDto
+ var (percentage, totalTime) = await CalculateTaskPercentage(task, cancellationToken);
+ 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,
Name = task.Name,
Level = ProjectHierarchyLevel.Task,
ParentId = task.PhaseId,
- Percentage = percentage
+ Percentage = percentage,
+ TotalHours = (int)totalTime.TotalHours,
+ Minutes = totalTime.Minutes,
+ SpentTime = spentTime,
+ RemainingTime = remainingTime,
+ Sections = sectionDtos,
+ Priority = task.Priority
});
}
-
return result;
}
- private async Task SetSkillFlags(List projects, CancellationToken cancellationToken)
+ private async Task SetSkillFlags(List items, CancellationToken cancellationToken) where TItem : GetProjectItemDto
{
- if (!projects.Any())
+ if (!items.Any())
return;
-
- var projectIds = projects.Select(x => x.Id).ToList();
- var hierarchyLevel = projects.First().Level;
-
+ var ids = items.Select(x => x.Id).ToList();
+ var hierarchyLevel = items.First().Level;
switch (hierarchyLevel)
{
case ProjectHierarchyLevel.Project:
- await SetSkillFlagsForProjects(projects, projectIds, cancellationToken);
+ await SetSkillFlagsForProjects(items, ids, cancellationToken);
break;
-
case ProjectHierarchyLevel.Phase:
- await SetSkillFlagsForPhases(projects, projectIds, cancellationToken);
- break;
-
- case ProjectHierarchyLevel.Task:
- await SetSkillFlagsForTasks(projects, projectIds, cancellationToken);
+ await SetSkillFlagsForPhases(items, ids, cancellationToken);
break;
}
}
- private async Task SetSkillFlagsForProjects(List projects, List projectIds, CancellationToken cancellationToken)
+
+ private async Task SetSkillFlagsForProjects(List items, List projectIds, CancellationToken cancellationToken) where TItem : GetProjectItemDto
{
- var projectSections = await _context.ProjectSections
- .Include(x => x.Skill)
- .Where(s => projectIds.Contains(s.ProjectId))
+ // For projects: gather all phases, then tasks, then sections
+ var phases = await _context.ProjectPhases
+ .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);
- if (!projectSections.Any())
- return;
-
- foreach (var project in projects)
+ foreach (var item in items)
{
- var sections = projectSections.Where(s => s.ProjectId == project.Id).ToList();
- project.HasBackend = sections.Any(x => x.Skill?.Name == "Backend");
- project.HasFront = sections.Any(x => x.Skill?.Name == "Frontend");
- project.HasDesign = sections.Any(x => x.Skill?.Name == "UI/UX Design");
+ var relatedPhases = phases; // used for filtering tasks by project
+ var relatedTasks = await _context.ProjectTasks
+ .Where(t => t.PhaseId != Guid.Empty && relatedPhases.Contains(t.PhaseId))
+ .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 projects, List phaseIds, CancellationToken cancellationToken)
+ private async Task SetSkillFlagsForPhases(List items, List phaseIds, CancellationToken cancellationToken) where TItem : GetProjectItemDto
{
- var phaseSections = await _context.PhaseSections
- .Include(x => x.Skill)
- .Where(s => phaseIds.Contains(s.PhaseId))
+ // For phases: gather tasks, then sections
+ var tasks = await _context.ProjectTasks
+ .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);
- if (!phaseSections.Any())
- return;
-
- foreach (var phase in projects)
+ foreach (var item in items)
{
- var sections = phaseSections.Where(s => s.PhaseId == phase.Id).ToList();
- phase.HasBackend = sections.Any(x => x.Skill?.Name == "Backend");
- phase.HasFront = sections.Any(x => x.Skill?.Name == "Frontend");
- phase.HasDesign = sections.Any(x => x.Skill?.Name == "UI/UX Design");
+ // Filter tasks for this phase
+ var phaseTaskIds = await _context.ProjectTasks
+ .Where(t => t.PhaseId == item.Id)
+ .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"));
+ 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 SetSkillFlagsForTasks(List projects, List taskIds, CancellationToken cancellationToken)
+ private async Task<(int Percentage, TimeSpan TotalTime)> CalculateProjectPercentage(Project project, CancellationToken cancellationToken)
{
- 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 CalculateProjectPercentage(Project project, CancellationToken cancellationToken)
- {
- // گرفتن تمام فازهای پروژه
var phases = await _context.ProjectPhases
.Where(ph => ph.ProjectId == project.Id)
.ToListAsync(cancellationToken);
-
if (!phases.Any())
- return 0;
-
- // محاسبه درصد هر فاز و میانگینگیری
+ return (0, TimeSpan.Zero);
var phasePercentages = new List();
+ var totalTime = TimeSpan.Zero;
foreach (var phase in phases)
{
- var phasePercentage = await CalculatePhasePercentage(phase, cancellationToken);
+ var (phasePercentage, phaseTime) = await CalculatePhasePercentage(phase, cancellationToken);
phasePercentages.Add(phasePercentage);
+ totalTime += phaseTime;
}
-
- return phasePercentages.Any() ? (int)phasePercentages.Average() : 0;
+ var averagePercentage = phasePercentages.Any() ? (int)phasePercentages.Average() : 0;
+ return (averagePercentage, totalTime);
}
- private async Task CalculatePhasePercentage(ProjectPhase phase, CancellationToken cancellationToken)
+ private async Task<(int Percentage, TimeSpan TotalTime)> CalculatePhasePercentage(ProjectPhase phase, CancellationToken cancellationToken)
{
- // گرفتن تمام تسکهای فاز
var tasks = await _context.ProjectTasks
.Where(t => t.PhaseId == phase.Id)
.ToListAsync(cancellationToken);
-
if (!tasks.Any())
- return 0;
-
- // محاسبه درصد هر تسک و میانگینگیری
+ return (0, TimeSpan.Zero);
var taskPercentages = new List();
+ var totalTime = TimeSpan.Zero;
foreach (var task in tasks)
{
- var taskPercentage = await CalculateTaskPercentage(task, cancellationToken);
+ var (taskPercentage, taskTime) = await CalculateTaskPercentage(task, cancellationToken);
taskPercentages.Add(taskPercentage);
+ totalTime += taskTime;
}
-
- return taskPercentages.Any() ? (int)taskPercentages.Average() : 0;
+ var averagePercentage = taskPercentages.Any() ? (int)taskPercentages.Average() : 0;
+ return (averagePercentage, totalTime);
}
- private async Task CalculateTaskPercentage(ProjectTask task, CancellationToken cancellationToken)
+ private async Task<(int Percentage, TimeSpan TotalTime)> CalculateTaskPercentage(ProjectTask task, CancellationToken cancellationToken)
{
- // گرفتن تمام سکشنهای تسک با activities
var sections = await _context.TaskSections
.Include(s => s.Activities)
+ .Include(x=>x.AdditionalTimes)
.Where(s => s.TaskId == task.Id)
.ToListAsync(cancellationToken);
-
if (!sections.Any())
- return 0;
-
- // محاسبه درصد هر سکشن و میانگینگیری
+ return (0, TimeSpan.Zero);
var sectionPercentages = new List();
+ var totalTime = TimeSpan.Zero;
foreach (var section in sections)
{
- var sectionPercentage = CalculateSectionPercentage(section);
+ var (sectionPercentage, sectionTime) = CalculateSectionPercentage(section);
sectionPercentages.Add(sectionPercentage);
+ totalTime += sectionTime;
}
-
- return sectionPercentages.Any() ? (int)sectionPercentages.Average() : 0;
+ var averagePercentage = sectionPercentages.Any() ? (int)sectionPercentages.Average() : 0;
+ return (averagePercentage, totalTime);
}
- private static int CalculateSectionPercentage(TaskSection section)
+ private static (int Percentage, TimeSpan TotalTime) CalculateSectionPercentage(TaskSection section)
{
- // محاسبه کل زمان تخمین زده شده (اولیه + اضافی)
- var totalEstimatedHours = section.FinalEstimatedHours.TotalHours;
+ return ((int)section.GetProgressPercentage(),section.FinalEstimatedHours);
+ }
- if (totalEstimatedHours <= 0)
- return 0;
+ private static AssignmentStatus GetAssignmentStatus(TaskSection? section)
+ {
+ // تعیین تکلیف نشده: section وجود ندارد
+ if (section == null)
+ return AssignmentStatus.Unassigned;
- // محاسبه کل زمان صرف شده از activities
- var totalSpentHours = section.Activities.Sum(a => a.GetTimeSpent().TotalHours);
+ // بررسی وجود user
+ bool hasUser = section.CurrentAssignedUserId > 0;
+
+ // بررسی وجود time (InitialEstimatedHours بزرگتر از صفر باشد)
+ bool hasTime = section.InitialEstimatedHours > TimeSpan.Zero;
- // محاسبه درصد (حداکثر 100%)
- var percentage = (totalSpentHours / totalEstimatedHours) * 100;
- return Math.Min((int)Math.Round(percentage), 100);
+ // تعیین تکلیف شده: هم user و هم time تعیین شده
+ if (hasUser && hasTime)
+ return AssignmentStatus.Assigned;
+
+ // فقط کاربر تعیین شده: user دارد ولی time ندارد
+ if (hasUser && !hasTime)
+ return AssignmentStatus.UserOnly;
+
+ // تعیین تکلیف نشده: نه user دارد نه time
+ return AssignmentStatus.Unassigned;
}
}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListResponse.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListResponse.cs
index 928501dd..f4934501 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListResponse.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetProjectsListResponse.cs
@@ -1,5 +1,6 @@
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
public record GetProjectsListResponse(
- List Projects);
-
+ List Projects,
+ List Phases,
+ List Tasks);
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetTaskListDto.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetTaskListDto.cs
new file mode 100644
index 00000000..0661cdb1
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetTaskListDto.cs
@@ -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 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; }
+
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListQuery.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListQuery.cs
index 2d42152a..73ea9a9e 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListQuery.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListQuery.cs
@@ -6,5 +6,6 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.Project
public record ProjectBoardListQuery: IBaseQuery>
{
+ public long? UserId { get; set; }
public TaskSectionStatus? Status { get; set; }
}
\ No newline at end of file
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListQueryHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListQueryHandler.cs
index 69e82940..4ff4f377 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListQueryHandler.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListQueryHandler.cs
@@ -40,6 +40,11 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler x.Status == request.Status);
}
+
+ if (request.UserId is > 0)
+ {
+ queryable = queryable.Where(x => x.CurrentAssignedUserId == request.UserId);
+ }
var data = await queryable.ToListAsync(cancellationToken);
@@ -53,68 +58,87 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler x.Id, x => x.FullName, cancellationToken);
- var result = data.Select(x =>
- {
- // محاسبه یکبار برای هر Activity و Cache کردن نتیجه
- var activityTimeData = x.Activities.Select(a =>
+ var result = data
+ .OrderByDescending(x => x.CurrentAssignedUserId == currentUserId)
+ .ThenByDescending(x=>x.Task.Priority)
+ .ThenBy(x => GetStatusOrder(x.Status))
+ .Select(x =>
{
- var timeSpent = a.GetTimeSpent();
- return new
+ // محاسبه یکبار برای هر Activity و Cache کردن نتیجه
+ var activityTimeData = x.Activities.Select(a =>
{
- Activity = a,
- TimeSpent = timeSpent,
- TotalSeconds = timeSpent.TotalSeconds,
- FormattedTime = timeSpent.ToString(@"hh\:mm")
+ var timeSpent = a.GetTimeSpent();
+ return new
+ {
+ Activity = a,
+ TimeSpent = timeSpent,
+ TotalSeconds = timeSpent.TotalSeconds,
+ FormattedTime = timeSpent.ToString(@"hh\:mm")
+ };
+ }).ToList();
+
+ // ادغام پشت سر هم فعالیتهای یک کاربر
+ var mergedHistories = new List();
+ foreach (var activityData in activityTimeData)
+ {
+ var lastHistory = mergedHistories.LastOrDefault();
+
+ // اگر آخرین history برای همین کاربر باشد، زمانها را جمع میکنیم
+ if (lastHistory != null && lastHistory.UserId == activityData.Activity.UserId)
+ {
+ var totalTimeSpan = lastHistory.WorkedTimeSpan + activityData.TimeSpent;
+ lastHistory.WorkedTimeSpan = totalTimeSpan;
+ lastHistory.WorkedTime = totalTimeSpan.ToString(@"hh\:mm");
+ }
+ else
+ {
+ // در غیر این صورت، یک history جدید اضافه میکنیم
+ mergedHistories.Add(new ProjectProgressHistoryDto()
+ {
+ UserId = activityData.Activity.UserId,
+ IsCurrentUser = activityData.Activity.UserId == currentUserId,
+ Name = users.GetValueOrDefault(activityData.Activity.UserId, "ناشناس"),
+ WorkedTime = activityData.FormattedTime,
+ WorkedTimeSpan = activityData.TimeSpent,
+ });
+ }
+ }
+ return new ProjectBoardListResponse()
+ {
+ Id = x.Id,
+ PhaseName = x.Task.Phase.Name,
+ ProjectName = x.Task.Phase.Project.Name,
+ TaskName = x.Task.Name,
+ SectionStatus = x.Status,
+ TaskPriority = x.Task.Priority,
+ Progress = new ProjectProgressDto()
+ {
+ CompleteSecond = x.FinalEstimatedHours.TotalSeconds,
+ Percentage = (int)x.GetProgressPercentage(),
+ CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds),
+ Histories = mergedHistories
+ },
+ OriginalUser = users.GetValueOrDefault(x.OriginalAssignedUserId, "ناشناس"),
+ AssignedUser = x.CurrentAssignedUserId == x.OriginalAssignedUserId ? null
+ : users.GetValueOrDefault(x.CurrentAssignedUserId, "ناشناس"),
+ SkillName = x.Skill?.Name??"-",
+ TaskId = x.TaskId
};
}).ToList();
- // ادغام پشت سر هم فعالیتهای یک کاربر
- var mergedHistories = new List();
- foreach (var activityData in activityTimeData)
- {
- var lastHistory = mergedHistories.LastOrDefault();
-
- // اگر آخرین history برای همین کاربر باشد، زمانها را جمع میکنیم
- if (lastHistory != null && lastHistory.UserId == activityData.Activity.UserId)
- {
- var totalTimeSpan = lastHistory.WorkedTimeSpan + activityData.TimeSpent;
- lastHistory.WorkedTimeSpan = totalTimeSpan;
- lastHistory.WorkedTime = totalTimeSpan.ToString(@"hh\:mm");
- }
- else
- {
- // در غیر این صورت، یک history جدید اضافه میکنیم
- mergedHistories.Add(new ProjectProgressHistoryDto()
- {
- UserId = activityData.Activity.UserId,
- IsCurrentUser = activityData.Activity.UserId == currentUserId,
- Name = users.GetValueOrDefault(activityData.Activity.UserId, "ناشناس"),
- WorkedTime = activityData.FormattedTime,
- WorkedTimeSpan = activityData.TimeSpent,
- });
- }
- }
-
- return new ProjectBoardListResponse()
- {
- Id = x.Id,
- PhaseName = x.Task.Phase.Name,
- ProjectName = x.Task.Phase.Project.Name,
- TaskName = x.Task.Name,
- SectionStatus = x.Status,
- Progress = new ProjectProgressDto()
- {
- CompleteSecond = x.FinalEstimatedHours.TotalSeconds,
- CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds),
- Histories = mergedHistories
- },
- OriginalUser = users.GetValueOrDefault(x.OriginalAssignedUserId, "ناشناس"),
- AssignedUser = x.CurrentAssignedUserId == x.OriginalAssignedUserId ? null
- : users.GetValueOrDefault(x.CurrentAssignedUserId, "ناشناس"),
- SkillName = x.Skill?.Name??"-",
- };
- }).ToList();
-
return OperationResult>.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
+ };
+ }
}
\ No newline at end of file
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListResponse.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListResponse.cs
index ab38750e..31cb70db 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListResponse.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListResponse.cs
@@ -13,11 +13,14 @@ public class ProjectBoardListResponse
public string? AssignedUser { get; set; }
public string OriginalUser { get; set; }
public string SkillName { get; set; }
+ public ProjectTaskPriority TaskPriority { get; set; }
+ public Guid TaskId { get; set; }
}
public class ProjectProgressDto
{
public double CurrentSecond { get; set; }
+ public int Percentage { get; set; }
public double CompleteSecond { get; set; }
public List Histories { get; set; }
}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectDeployBoardDetail/ProjectDeployBoardDetailsQueryHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectDeployBoardDetail/ProjectDeployBoardDetailsQueryHandler.cs
index 33a1caa6..07dade77 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectDeployBoardDetail/ProjectDeployBoardDetailsQueryHandler.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectDeployBoardDetail/ProjectDeployBoardDetailsQueryHandler.cs
@@ -12,14 +12,16 @@ public record ProjectDeployBoardDetailsResponse(
public record ProjectDeployBoardDetailPhaseItem(
string Name,
TimeSpan TotalTimeSpan,
- TimeSpan DoneTimeSpan);
+ TimeSpan DoneTimeSpan,
+ int Percentage);
public record ProjectDeployBoardDetailTaskItem(
string Name,
TimeSpan TotalTimeSpan,
TimeSpan DoneTimeSpan,
+ int Percentage,
List Skills)
- : ProjectDeployBoardDetailPhaseItem(Name, TotalTimeSpan, DoneTimeSpan);
+ : ProjectDeployBoardDetailPhaseItem(Name, TotalTimeSpan, DoneTimeSpan, Percentage);
public record ProjectDeployBoardDetailItemSkill(string OriginalUserFullName, string SkillName, int TimePercentage);
@@ -71,6 +73,7 @@ public class
var doneTime = t.Sections.Aggregate(TimeSpan.Zero,
(sum, next) => sum.Add(next.GetTotalTimeSpent()));
+
var skills = t.Sections
.Select(s =>
{
@@ -79,22 +82,30 @@ public class
var skillName = s.Skill?.Name ?? "بدون مهارت";
- var totalTimeSpent = s.GetTotalTimeSpent();
-
- var timePercentage = s.FinalEstimatedHours.Ticks > 0
- ? (int)((totalTimeSpent.Ticks / (double)s.FinalEstimatedHours.Ticks) * 100)
- : 0;
-
+ var timePercentage = (int)s.GetProgressPercentage();
+
return new ProjectDeployBoardDetailItemSkill(
originalUserFullName,
skillName,
timePercentage);
}).ToList();
+ int taskPercentage;
+
+ if (skills.Count == 0)
+ {
+ taskPercentage = 0;
+ }
+ else
+ {
+ taskPercentage = (int)Math.Round(skills.Average(x => x.TimePercentage));
+ }
+
return new ProjectDeployBoardDetailTaskItem(
t.Name,
totalTime,
doneTime,
+ taskPercentage,
skills);
}).ToList();
@@ -104,7 +115,10 @@ public class
var doneTimeSpan = tasksRes.Aggregate(TimeSpan.Zero,
(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);
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectDeployBoardList/ProjectDeployBoardListQueryHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectDeployBoardList/ProjectDeployBoardListQueryHandler.cs
index 6a0a7cda..d8f6e39d 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectDeployBoardList/ProjectDeployBoardListQueryHandler.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectDeployBoardList/ProjectDeployBoardListQueryHandler.cs
@@ -17,6 +17,7 @@ public record ProjectDeployBoardListItem()
public int DoneTasks { get; set; }
public TimeSpan TotalTimeSpan { get; set; }
public TimeSpan DoneTimeSpan { get; set; }
+ public int Percentage { get; set; }
public ProjectDeployStatus DeployStatus { get; set; }
}
public record GetProjectsDeployBoardListResponse(List Items);
@@ -66,7 +67,8 @@ public class ProjectDeployBoardListQueryHandler:IBaseQueryHandler x.TaskId).Distinct().Count(),
TotalTimeSpan = TimeSpan.FromTicks(g.Sum(x => x.InitialEstimatedHours.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();
var response = new GetProjectsDeployBoardListResponse(list);
return OperationResult.Success(response);
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQuery.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQuery.cs
index 4b2c08b9..a3ac883d 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQuery.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQuery.cs
@@ -3,21 +3,22 @@ using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails;
-public record ProjectSetTimeDetailsQuery(Guid TaskId)
+public record ProjectSetTimeDetailsQuery(Guid Id, ProjectHierarchyLevel Level)
: IBaseQuery;
public record ProjectSetTimeResponse(
- List SectionItems,
+ List SkillItems,
Guid Id,
ProjectHierarchyLevel Level);
-public record ProjectSetTimeResponseSections
+public record ProjectSetTimeResponseSkill
{
+ public Guid SkillId { get; init; }
public string SkillName { get; init; }
- public string UserName { get; init; }
- public int InitialTime { get; set; }
+ public long UserId { get; set; }
+ public string UserFullName { get; init; }
+ public int InitialHours { get; set; }
+ public int InitialMinutes { get; set; }
public string InitialDescription { get; set; }
- public int TotalEstimateTime { get; init; }
- public int TotalAdditionalTime { get; init; }
public string InitCreationTime { get; init; }
public List AdditionalTimes { get; init; }
public Guid SectionId { get; set; }
@@ -25,6 +26,8 @@ public record ProjectSetTimeResponseSections
public class ProjectSetTimeResponseSectionAdditionalTime
{
- public int Time { get; init; }
+ public int Hours { get; init; }
+ public int Minutes { get; init; }
public string Description { get; init; }
+ public string CreationDate { get; set; }
}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryHandler.cs
index f99f3370..aee31370 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryHandler.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryHandler.cs
@@ -22,17 +22,30 @@ public class ProjectSetTimeDetailsQueryHandler
public async Task> Handle(ProjectSetTimeDetailsQuery request,
CancellationToken cancellationToken)
+ {
+ return request.Level switch
+ {
+ ProjectHierarchyLevel.Task => await GetTaskSetTimeDetails(request.Id, request.Level, cancellationToken),
+ ProjectHierarchyLevel.Phase => await GetPhaseSetTimeDetails(request.Id, request.Level, cancellationToken),
+ ProjectHierarchyLevel.Project => await GetProjectSetTimeDetails(request.Id, request.Level, cancellationToken),
+ _ => OperationResult.Failure("سطح معادل نامعتبر است")
+ };
+ }
+
+ private async Task> GetTaskSetTimeDetails(Guid id, ProjectHierarchyLevel level,
+ CancellationToken cancellationToken)
{
var task = await _context.ProjectTasks
- .Where(p => p.Id == request.TaskId)
+ .Where(p => p.Id == id)
.Include(x => x.Sections)
.ThenInclude(x => x.AdditionalTimes).AsNoTracking()
.FirstOrDefaultAsync(cancellationToken);
if (task == null)
{
- return OperationResult.NotFound("Project not found");
+ return OperationResult.NotFound("تسک یافت نشد");
}
+
var userIds = task.Sections.Select(x => x.OriginalAssignedUserId)
.Distinct().ToList();
@@ -40,40 +53,142 @@ public class ProjectSetTimeDetailsQueryHandler
.Where(x => userIds.Contains(x.Id))
.AsNoTracking()
.ToListAsync(cancellationToken);
- var skillIds = task.Sections.Select(x => x.SkillId)
- .Distinct().ToList();
- var skills = await _context.Skills
- .Where(x => skillIds.Contains(x.Id))
+ var skills = await _context.Skills
.AsNoTracking()
.ToListAsync(cancellationToken);
var res = new ProjectSetTimeResponse(
- task.Sections.Select(ts =>
+ skills.Select(skill =>
+ {
+ var section = task.Sections
+ .FirstOrDefault(x => x.SkillId == skill.Id);
+ var user = users.FirstOrDefault(x => x.Id == section?.OriginalAssignedUserId);
+ return new ProjectSetTimeResponseSkill
{
- var user = users.FirstOrDefault(x => x.Id == ts.OriginalAssignedUserId);
- var skill = skills.FirstOrDefault(x => x.Id == ts.SkillId);
- return new ProjectSetTimeResponseSections
- {
- AdditionalTimes = ts.AdditionalTimes
- .Select(x => new ProjectSetTimeResponseSectionAdditionalTime
- {
- Description = x.Reason ?? "",
- Time = (int)x.Hours.TotalHours
- }).ToList(),
- InitCreationTime = ts.CreationDate.ToFarsi(),
- SkillName = skill?.Name ?? "",
- TotalAdditionalTime = (int)ts.GetTotalAdditionalTime().TotalHours,
- TotalEstimateTime = (int)ts.FinalEstimatedHours.TotalHours,
- UserName = user?.UserName ?? "",
- SectionId = ts.Id,
- InitialDescription = ts.InitialDescription ?? "",
- InitialTime = (int)ts.InitialEstimatedHours.TotalHours
- };
- }).ToList(),
- task.Id,
- ProjectHierarchyLevel.Task);
+ AdditionalTimes = section?.AdditionalTimes
+ .Select(x => new ProjectSetTimeResponseSectionAdditionalTime
+ {
+ Description = x.Reason ?? "",
+ Hours = (int)x.Hours.TotalHours,
+ Minutes = x.Hours.Minutes,
+ CreationDate = x.CreationDate.ToFarsi()
+ }).OrderBy(x => x.CreationDate).ToList() ?? [],
+ InitCreationTime = section?.CreationDate.ToFarsi() ?? "",
+ SkillName = skill.Name ?? "",
+ UserFullName = user?.FullName ?? "",
+ SectionId = section?.Id ?? Guid.Empty,
+ InitialDescription = section?.InitialDescription ?? "",
+ InitialHours = (int)(section?.InitialEstimatedHours.TotalHours ?? 0),
+ InitialMinutes = section?.InitialEstimatedHours.Minutes ?? 0,
+ UserId = section?.OriginalAssignedUserId ?? 0,
+ SkillId = skill.Id,
+ };
+ }).OrderBy(x => x.SkillId).ToList(),
+ task.Id,
+ level);
+ return OperationResult.Success(res);
+ }
+
+ private async Task> GetPhaseSetTimeDetails(Guid id, ProjectHierarchyLevel level,
+ CancellationToken cancellationToken)
+ {
+ var phase = await _context.ProjectPhases
+ .Where(p => p.Id == id)
+ .Include(x => x.PhaseSections).AsNoTracking()
+ .FirstOrDefaultAsync(cancellationToken);
+
+ if (phase == null)
+ {
+ return OperationResult.NotFound("فاز یافت نشد");
+ }
+
+ var userIds = phase.PhaseSections.Select(x => x.UserId)
+ .Distinct().ToList();
+
+ var users = await _context.Users
+ .Where(x => userIds.Contains(x.Id))
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+
+ var skills = await _context.Skills
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+
+ var res = new ProjectSetTimeResponse(
+ skills.Select(skill =>
+ {
+ var section = phase.PhaseSections
+ .FirstOrDefault(x => x.SkillId == skill.Id);
+ var user = users.FirstOrDefault(x => x.Id == section?.UserId);
+ return new ProjectSetTimeResponseSkill
+ {
+ AdditionalTimes = [],
+ InitCreationTime = "",
+ SkillName = skill.Name ?? "",
+ UserFullName = user?.FullName ?? "",
+ SectionId = section?.Id ?? Guid.Empty,
+ InitialDescription = "",
+ InitialHours = 0,
+ InitialMinutes = 0,
+ UserId = section?.UserId ?? 0,
+ SkillId = skill.Id,
+ };
+ }).OrderBy(x => x.SkillId).ToList(),
+ phase.Id,
+ level);
+
+ return OperationResult.Success(res);
+ }
+
+ private async Task> GetProjectSetTimeDetails(Guid id, ProjectHierarchyLevel level,
+ CancellationToken cancellationToken)
+ {
+ var project = await _context.Projects
+ .Where(p => p.Id == id)
+ .Include(x => x.ProjectSections).AsNoTracking()
+ .FirstOrDefaultAsync(cancellationToken);
+
+ if (project == null)
+ {
+ return OperationResult.NotFound("پروژه یافت نشد");
+ }
+
+ var userIds = project.ProjectSections.Select(x => x.UserId)
+ .Distinct().ToList();
+
+ var users = await _context.Users
+ .Where(x => userIds.Contains(x.Id))
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+
+ var skills = await _context.Skills
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+
+ var res = new ProjectSetTimeResponse(
+ skills.Select(skill =>
+ {
+ var section = project.ProjectSections
+ .FirstOrDefault(x => x.SkillId == skill.Id);
+ var user = users.FirstOrDefault(x => x.Id == section?.UserId);
+ return new ProjectSetTimeResponseSkill
+ {
+ AdditionalTimes = [],
+ InitCreationTime = "",
+ SkillName = skill.Name ?? "",
+ UserFullName = user?.FullName ?? "",
+ SectionId = section?.Id ?? Guid.Empty,
+ InitialDescription = "",
+ InitialHours = 0,
+ InitialMinutes = 0,
+ UserId = section?.UserId ?? 0,
+ SkillId = skill.Id,
+ };
+ }).OrderBy(x => x.SkillId).ToList(),
+ project.Id,
+ level);
return OperationResult.Success(res);
}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryValidator.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryValidator.cs
index 13952f46..bd2c0dbe 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryValidator.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectSetTimeDetails/ProjectSetTimeDetailsQueryValidator.cs
@@ -1,13 +1,18 @@
using FluentValidation;
+using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails;
-public class ProjectSetTimeDetailsQueryValidator:AbstractValidator
+public class ProjectSetTimeDetailsQueryValidator : AbstractValidator
{
public ProjectSetTimeDetailsQueryValidator()
{
- RuleFor(x => x.TaskId)
+ RuleFor(x => x.Id)
.NotEmpty()
- .WithMessage("شناسه پروژه نمیتواند خالی باشد.");
+ .WithMessage("شناسه نمیتواند خالی باشد.");
+
+ RuleFor(x => x.Level)
+ .IsInEnum()
+ .WithMessage("سطح معادل نامعتبر است.");
}
}
\ No newline at end of file
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/DeleteMessage/DeleteMessageCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/DeleteMessage/DeleteMessageCommand.cs
new file mode 100644
index 00000000..68aaabef
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/DeleteMessage/DeleteMessageCommand.cs
@@ -0,0 +1,47 @@
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.DeleteMessage;
+
+public record DeleteMessageCommand(Guid MessageId) : IBaseCommand;
+
+public class DeleteMessageCommandHandler : IBaseCommandHandler
+{
+ private readonly ITaskChatMessageRepository _repository;
+ private readonly IAuthHelper _authHelper;
+
+ public DeleteMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
+ {
+ _repository = repository;
+ _authHelper = authHelper;
+ }
+
+ public async Task Handle(DeleteMessageCommand request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId()??
+ throw new UnAuthorizedException("کاربر احراز هویت نشده است");
+
+ var message = await _repository.GetByIdAsync(request.MessageId);
+ if (message == null)
+ {
+ return OperationResult.NotFound("پیام یافت نشد");
+ }
+
+ try
+ {
+ message.DeleteMessage(currentUserId);
+ await _repository.UpdateAsync(message);
+ await _repository.SaveChangesAsync();
+
+ // TODO: SignalR notification
+
+ return OperationResult.Success();
+ }
+ catch (Exception ex)
+ {
+ return OperationResult.ValidationError(ex.Message);
+ }
+ }
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/EditMessage/EditMessageCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/EditMessage/EditMessageCommand.cs
new file mode 100644
index 00000000..2066a769
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/EditMessage/EditMessageCommand.cs
@@ -0,0 +1,51 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
+using MediatR;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.EditMessage;
+
+public record EditMessageCommand(
+ Guid MessageId,
+ string NewTextContent
+) : IBaseCommand;
+
+public class EditMessageCommandHandler : IBaseCommandHandler
+{
+ private readonly ITaskChatMessageRepository _repository;
+ private readonly IAuthHelper _authHelper;
+
+ public EditMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
+ {
+ _repository = repository;
+ _authHelper = authHelper;
+ }
+
+ public async Task Handle(EditMessageCommand request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId()??
+ throw new UnAuthorizedException("کاربر احراز هویت نشده است");
+
+ var message = await _repository.GetByIdAsync(request.MessageId);
+ if (message == null)
+ {
+ return OperationResult.NotFound("پیام یافت نشد");
+ }
+
+ try
+ {
+ message.EditMessage(request.NewTextContent, currentUserId);
+ await _repository.UpdateAsync(message);
+ await _repository.SaveChangesAsync();
+
+ // TODO: SignalR notification
+
+ return OperationResult.Success();
+ }
+ catch (Exception ex)
+ {
+ return OperationResult.ValidationError(ex.Message);
+ }
+ }
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/PinMessage/PinMessageCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/PinMessage/PinMessageCommand.cs
new file mode 100644
index 00000000..9841df53
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/PinMessage/PinMessageCommand.cs
@@ -0,0 +1,46 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
+using MediatR;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.PinMessage;
+
+public record PinMessageCommand(Guid MessageId) : IBaseCommand;
+
+public class PinMessageCommandHandler : IBaseCommandHandler
+{
+ private readonly ITaskChatMessageRepository _repository;
+ private readonly IAuthHelper _authHelper;
+
+ public PinMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
+ {
+ _repository = repository;
+ _authHelper = authHelper;
+ }
+
+ public async Task Handle(PinMessageCommand request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId()??
+ throw new UnAuthorizedException("کاربر احراز هویت نشده است");
+
+ var message = await _repository.GetByIdAsync(request.MessageId);
+ if (message == null)
+ {
+ return OperationResult.NotFound("پیام یافت نشد");
+ }
+
+ try
+ {
+ message.PinMessage(currentUserId);
+ await _repository.UpdateAsync(message);
+ await _repository.SaveChangesAsync();
+
+ return OperationResult.Success();
+ }
+ catch (Exception ex)
+ {
+ return OperationResult.ValidationError(ex.Message);
+ }
+ }
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs
new file mode 100644
index 00000000..245d8b39
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs
@@ -0,0 +1,212 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
+using GozareshgirProgramManager.Application.Services.FileManagement;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
+using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
+using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
+using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
+using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
+using MediatR;
+using Microsoft.AspNetCore.Http;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.SendMessage;
+
+public record SendMessageCommand(
+ Guid TaskId,
+ MessageType MessageType,
+ string? TextContent,
+ IFormFile? File,
+ Guid? ReplyToMessageId
+) : IBaseCommand;
+
+public class SendMessageCommandHandler : IBaseCommandHandler
+{
+ private readonly ITaskChatMessageRepository _messageRepository;
+ private readonly IUploadedFileRepository _fileRepository;
+ private readonly IProjectTaskRepository _taskRepository;
+ private readonly IFileStorageService _fileStorageService;
+ private readonly IThumbnailGeneratorService _thumbnailService;
+ private readonly IAuthHelper _authHelper;
+
+ public SendMessageCommandHandler(
+ ITaskChatMessageRepository messageRepository,
+ IUploadedFileRepository fileRepository,
+ IProjectTaskRepository taskRepository,
+ IFileStorageService fileStorageService,
+ IThumbnailGeneratorService thumbnailService, IAuthHelper authHelper)
+ {
+ _messageRepository = messageRepository;
+ _fileRepository = fileRepository;
+ _taskRepository = taskRepository;
+ _fileStorageService = fileStorageService;
+ _thumbnailService = thumbnailService;
+ _authHelper = authHelper;
+ }
+
+ public async Task> Handle(SendMessageCommand request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId()
+ ?? throw new UnAuthorizedException("کاربر احراز هویت نشده است");
+
+ var task = await _taskRepository.GetByIdAsync(request.TaskId, cancellationToken);
+ if (task == null)
+ {
+ return OperationResult.NotFound("تسک یافت نشد");
+ }
+
+ Guid? uploadedFileId = null;
+ if (request.File != null)
+ {
+ if (request.File.Length == 0)
+ {
+ return OperationResult.ValidationError("فایل خالی است");
+ }
+
+ const long maxFileSize = 100 * 1024 * 1024;
+ if (request.File.Length > maxFileSize)
+ {
+ return OperationResult.ValidationError("حجم فایل بیش از حد مجاز است (حداکثر 100MB)");
+ }
+
+ var fileType = DetectFileType(request.File.ContentType, Path.GetExtension(request.File.FileName));
+
+ var uploadedFile = new UploadedFile(
+ originalFileName: request.File.FileName,
+ fileSizeBytes: request.File.Length,
+ mimeType: request.File.ContentType,
+ fileType: fileType,
+ category: FileCategory.TaskChatMessage,
+ uploadedByUserId: currentUserId,
+ storageProvider: StorageProvider.LocalFileSystem
+ );
+
+ await _fileRepository.AddAsync(uploadedFile);
+ await _fileRepository.SaveChangesAsync();
+
+ try
+ {
+ using var stream = request.File.OpenReadStream();
+ var uploadResult = await _fileStorageService.UploadAsync(
+ stream,
+ uploadedFile.UniqueFileName,
+ "TaskChatMessage"
+ );
+
+ uploadedFile.CompleteUpload(uploadResult.StoragePath, uploadResult.StorageUrl);
+
+ if (fileType == FileType.Image)
+ {
+ var dimensions = await _thumbnailService.GetImageDimensionsAsync(uploadResult.StoragePath);
+ if (dimensions.HasValue)
+ {
+ uploadedFile.SetImageDimensions(dimensions.Value.Width, dimensions.Value.Height);
+ }
+
+ var thumbnail = await _thumbnailService
+ .GenerateImageThumbnailAsync(uploadResult.StoragePath, category: "TaskChatMessage");
+ if (thumbnail.HasValue)
+ {
+ uploadedFile.SetThumbnail(thumbnail.Value.ThumbnailUrl);
+ }
+ }
+
+ await _fileRepository.UpdateAsync(uploadedFile);
+ await _fileRepository.SaveChangesAsync();
+
+ uploadedFileId = uploadedFile.Id;
+ }
+ catch (Exception ex)
+ {
+ await _fileRepository.DeleteAsync(uploadedFile);
+ await _fileRepository.SaveChangesAsync();
+
+ return OperationResult.ValidationError($"خطا در آپلود فایل: {ex.Message}");
+ }
+ }
+
+ var message = new TaskChatMessage(
+ taskId: request.TaskId,
+ senderUserId: currentUserId,
+ messageType: request.MessageType,
+ textContent: request.TextContent,
+ uploadedFileId
+ );
+
+ if (request.ReplyToMessageId.HasValue)
+ {
+ message.SetReplyTo(request.ReplyToMessageId.Value);
+ }
+
+ await _messageRepository.AddAsync(message);
+ await _messageRepository.SaveChangesAsync();
+
+ if (uploadedFileId.HasValue)
+ {
+ var file = await _fileRepository.GetByIdAsync(uploadedFileId.Value);
+ if (file != null)
+ {
+ file.SetReference("TaskChatMessage", message.Id.ToString());
+ await _fileRepository.UpdateAsync(file);
+ await _fileRepository.SaveChangesAsync();
+ }
+ }
+
+ var dto = new MessageDto
+ {
+ Id = message.Id,
+ TaskId = message.TaskId,
+ SenderUserId = message.SenderUserId,
+ SenderName = "کاربر",
+ MessageType = message.MessageType.ToString(),
+ TextContent = message.TextContent,
+ ReplyToMessageId = message.ReplyToMessageId,
+ IsEdited = message.IsEdited,
+ IsPinned = message.IsPinned,
+ CreationDate = message.CreationDate,
+ IsMine = true
+ };
+
+ if (uploadedFileId.HasValue)
+ {
+ var file = await _fileRepository.GetByIdAsync(uploadedFileId.Value);
+ if (file != null)
+ {
+ dto.File = new MessageFileDto
+ {
+ Id = file.Id,
+ FileName = file.OriginalFileName,
+ FileUrl = file.StorageUrl ?? "",
+ FileSizeBytes = file.FileSizeBytes,
+ FileType = file.FileType.ToString(),
+ ThumbnailUrl = file.ThumbnailUrl,
+ ImageWidth = file.ImageWidth,
+ ImageHeight = file.ImageHeight,
+ DurationSeconds = file.DurationSeconds
+ };
+ }
+ }
+
+ return OperationResult.Success(dto);
+ }
+
+ private FileType DetectFileType(string mimeType, string extension)
+ {
+ if (mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
+ return FileType.Image;
+
+ if (mimeType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
+ return FileType.Video;
+
+ if (mimeType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase))
+ return FileType.Audio;
+
+ if (new[] { ".zip", ".rar", ".7z", ".tar", ".gz" }.Contains(extension.ToLower()))
+ return FileType.Archive;
+
+ return FileType.Document;
+ }
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/UnpinMessage/UnpinMessageCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/UnpinMessage/UnpinMessageCommand.cs
new file mode 100644
index 00000000..664087cd
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/UnpinMessage/UnpinMessageCommand.cs
@@ -0,0 +1,46 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
+using MediatR;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.UnpinMessage;
+
+public record UnpinMessageCommand(Guid MessageId) : IBaseCommand;
+
+public class UnpinMessageCommandHandler : IBaseCommandHandler
+{
+ private readonly ITaskChatMessageRepository _repository;
+ private readonly IAuthHelper _authHelper;
+
+ public UnpinMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
+ {
+ _repository = repository;
+ _authHelper = authHelper;
+ }
+
+ public async Task Handle(UnpinMessageCommand request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId()??
+ throw new UnAuthorizedException("کاربر احراز هویت نشده است");
+
+ var message = await _repository.GetByIdAsync(request.MessageId);
+ if (message == null)
+ {
+ return OperationResult.NotFound("پیام یافت نشد");
+ }
+
+ try
+ {
+ message.UnpinMessage(currentUserId);
+ await _repository.UpdateAsync(message);
+ await _repository.SaveChangesAsync();
+
+ return OperationResult.Success();
+ }
+ catch (Exception ex)
+ {
+ return OperationResult.ValidationError(ex.Message);
+ }
+ }
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/DTOs/MessageDto.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/DTOs/MessageDto.cs
new file mode 100644
index 00000000..67598d8c
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/DTOs/MessageDto.cs
@@ -0,0 +1,63 @@
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
+
+public class SendMessageDto
+{
+ public Guid TaskId { get; set; }
+ public string MessageType { get; set; } = string.Empty; // "Text", "File", "Image", "Voice", "Video"
+ public string? TextContent { get; set; }
+ public Guid? FileId { get; set; }
+ public Guid? ReplyToMessageId { get; set; }
+}
+
+public class MessageDto
+{
+ public Guid Id { get; set; }
+ public Guid TaskId { get; set; }
+ public long SenderUserId { get; set; }
+ public string SenderName { get; set; } = string.Empty;
+ public string MessageType { get; set; } = string.Empty;
+ public string? TextContent { get; set; }
+ public MessageFileDto? File { get; set; }
+ public Guid? ReplyToMessageId { get; set; }
+ public MessageDto? ReplyToMessage { get; set; }
+ public bool IsEdited { get; set; }
+ public DateTime? EditedDate { get; set; }
+ public bool IsPinned { get; set; }
+ public DateTime? PinnedDate { get; set; }
+ public long? PinnedByUserId { get; set; }
+ public DateTime CreationDate { get; set; }
+ public bool IsMine { get; set; }
+}
+
+public class MessageFileDto
+{
+ public Guid Id { get; set; }
+ public string FileName { get; set; } = string.Empty;
+ public string FileUrl { get; set; } = string.Empty;
+ public long FileSizeBytes { get; set; }
+ public string FileType { get; set; } = string.Empty;
+ public string? ThumbnailUrl { get; set; }
+ public int? ImageWidth { get; set; }
+ public int? ImageHeight { get; set; }
+ public int? DurationSeconds { get; set; }
+
+ public string FileSizeFormatted
+ {
+ get
+ {
+ const long kb = 1024;
+ const long mb = kb * 1024;
+ const long gb = mb * 1024;
+
+ if (FileSizeBytes >= gb)
+ return $"{FileSizeBytes / (double)gb:F2} GB";
+ if (FileSizeBytes >= mb)
+ return $"{FileSizeBytes / (double)mb:F2} MB";
+ if (FileSizeBytes >= kb)
+ return $"{FileSizeBytes / (double)kb:F2} KB";
+
+ return $"{FileSizeBytes} Bytes";
+ }
+ }
+}
+
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetMessages/GetMessagesQuery.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetMessages/GetMessagesQuery.cs
new file mode 100644
index 00000000..9d0deccf
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetMessages/GetMessagesQuery.cs
@@ -0,0 +1,208 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
+using Microsoft.EntityFrameworkCore;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetMessages;
+
+public record GetMessagesQuery(
+ Guid TaskId,
+ int Page = 1,
+ int PageSize = 50
+) : IBaseQuery>;
+
+
+
+public class GetMessagesQueryHandler : IBaseQueryHandler>
+{
+ private readonly IProgramManagerDbContext _context;
+ private readonly IAuthHelper _authHelper;
+
+ public GetMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper)
+ {
+ _context = context;
+ _authHelper = authHelper;
+ }
+
+ private List CreateAdditionalTimeNotes(
+ IEnumerable additionalTimes,
+ Dictionary users,
+ Guid taskId)
+ {
+ var notes = new List();
+
+ 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>> Handle(GetMessagesQuery request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId();
+
+ var skip = (request.Page - 1) * request.PageSize;
+
+ var query = _context.TaskChatMessages
+ .Where(m => m.TaskId == request.TaskId && !m.IsDeleted)
+ .Include(m => m.ReplyToMessage)
+ .OrderBy(m => m.CreationDate);
+
+ var totalCount = await query.CountAsync(cancellationToken);
+
+ var messages = await query
+ .Skip(skip)
+ .Take(request.PageSize)
+ .ToListAsync(cancellationToken);
+
+ // ✅ گرفتن تمامی کاربران برای نمایش نام کامل فرستنده به جای "کاربر"
+ var senderUserIds = messages.Select(m => m.SenderUserId).Distinct().ToList();
+ var users = await _context.Users
+ .Where(u => senderUserIds.Contains(u.Id))
+ .ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
+
+ // ✅ گرفتن تمامی زمانهای اضافی (Additional Times) برای نمایش به صورت نوت
+ var taskSections = await _context.TaskSections
+ .Where(ts => ts.TaskId == request.TaskId)
+ .Include(ts => ts.AdditionalTimes)
+ .ToListAsync(cancellationToken);
+
+ // ✅ تمام زمانهای اضافی را یکجا بگیر و مرتب کن
+ var allAdditionalTimes = taskSections
+ .SelectMany(ts => ts.AdditionalTimes)
+ .OrderBy(at => at.CreationDate)
+ .ToList();
+
+ var messageDtos = new List();
+
+ // ✅ ابتدا زمانهای اضافی قبل از اولین پیام را اضافه کن (اگر پیامی وجود داشته باشد)
+ 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)
+ {
+ // ✅ نام فرستنده را از Dictionary Users بگیر، در صورت عدم وجود "کاربر ناشناس" نمایش بده
+ var senderName = users.GetValueOrDefault(message.SenderUserId, "کاربر ناشناس");
+
+ var dto = new MessageDto
+ {
+ Id = message.Id,
+ TaskId = message.TaskId,
+ SenderUserId = message.SenderUserId,
+ SenderName = senderName,
+ MessageType = message.MessageType.ToString(),
+ TextContent = message.TextContent,
+ ReplyToMessageId = message.ReplyToMessageId,
+ IsEdited = message.IsEdited,
+ EditedDate = message.EditedDate,
+ IsPinned = message.IsPinned,
+ PinnedDate = message.PinnedDate,
+ PinnedByUserId = message.PinnedByUserId,
+ CreationDate = message.CreationDate,
+ IsMine = message.SenderUserId == currentUserId
+ };
+
+ if (message.ReplyToMessage != null)
+ {
+ var replySenderName = users.GetValueOrDefault(message.ReplyToMessage.SenderUserId, "کاربر ناشناس");
+
+ dto.ReplyToMessage = new MessageDto
+ {
+ Id = message.ReplyToMessage.Id,
+ SenderUserId = message.ReplyToMessage.SenderUserId,
+ SenderName = replySenderName,
+ TextContent = message.ReplyToMessage.TextContent,
+ CreationDate = message.ReplyToMessage.CreationDate
+ };
+ }
+
+ if (message.FileId.HasValue)
+ {
+ var file = await _context.UploadedFiles.FirstOrDefaultAsync(f => f.Id == message.FileId.Value, cancellationToken);
+ if (file != null)
+ {
+ dto.File = new MessageFileDto
+ {
+ Id = file.Id,
+ FileName = file.OriginalFileName,
+ FileUrl = file.StorageUrl ?? "",
+ FileSizeBytes = file.FileSizeBytes,
+ FileType = file.FileType.ToString(),
+ ThumbnailUrl = file.ThumbnailUrl,
+ ImageWidth = file.ImageWidth,
+ ImageHeight = file.ImageHeight,
+ DurationSeconds = file.DurationSeconds
+ };
+ }
+ }
+
+ messageDtos.Add(dto);
+
+ // ✅ پیدا کردن پیام بعدی (اگر وجود داشته باشد)
+ var currentIndex = messages.IndexOf(message);
+ var nextMessage = currentIndex < messages.Count - 1 ? messages[currentIndex + 1] : null;
+
+ if (nextMessage != null)
+ {
+ // ✅ زمانهای اضافی بین این پیام و پیام بعدی
+ var additionalTimesBetween = allAdditionalTimes
+ .Where(at => at.CreationDate > message.CreationDate && at.CreationDate < nextMessage.CreationDate)
+ .ToList();
+
+ messageDtos.AddRange(CreateAdditionalTimeNotes(additionalTimesBetween, users, request.TaskId));
+ }
+ else
+ {
+ // ✅ این آخرین پیام است، زمانهای اضافی بعد از آن را اضافه کن
+ var additionalTimesAfterLastMessage = allAdditionalTimes
+ .Where(at => at.CreationDate > message.CreationDate)
+ .ToList();
+
+ messageDtos.AddRange(CreateAdditionalTimeNotes(additionalTimesAfterLastMessage, users, request.TaskId));
+ }
+ }
+
+ // ✅ مرتب کردن نهایی تمام پیامها (معمولی + نوتها) بر اساس زمان ایجاد
+ messageDtos = messageDtos.OrderBy(m => m.CreationDate).ToList();
+
+ var response = new PaginationResult()
+ {
+ List = messageDtos,
+ TotalCount = totalCount,
+ };
+
+ return OperationResult>.Success(response);
+ }
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetPinnedMessages/GetPinnedMessagesQuery.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetPinnedMessages/GetPinnedMessagesQuery.cs
new file mode 100644
index 00000000..15e1cf40
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetPinnedMessages/GetPinnedMessagesQuery.cs
@@ -0,0 +1,82 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetPinnedMessages;
+
+public record GetPinnedMessagesQuery(Guid TaskId) : IBaseQuery>;
+
+public class GetPinnedMessagesQueryHandler : IBaseQueryHandler>
+{
+ private readonly IProgramManagerDbContext _context;
+ private readonly IAuthHelper _authHelper;
+
+ public GetPinnedMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper)
+ {
+ _context = context;
+ _authHelper = authHelper;
+ }
+
+ public async Task>> Handle(GetPinnedMessagesQuery request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId();
+
+ var messages = await _context.TaskChatMessages
+ .Where(m => m.TaskId == request.TaskId && m.IsPinned && !m.IsDeleted)
+ .Include(m => m.ReplyToMessage)
+ .OrderByDescending(m => m.PinnedDate)
+ .ToListAsync(cancellationToken);
+
+ // ✅ گرفتن تمامی کاربران برای نمایش نام کامل فرستنده
+ var senderUserIds = messages.Select(m => m.SenderUserId).Distinct().ToList();
+ var users = await _context.Users
+ .Where(u => senderUserIds.Contains(u.Id))
+ .ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
+
+ var messageDtos = new List();
+
+ foreach (var message in messages)
+ {
+ // ✅ نام فرستنده را از User واقعی بگیر (به جای "کاربر" ثابت)
+ var senderName = users.GetValueOrDefault(message.SenderUserId, "کاربر ناشناس");
+
+ var dto = new MessageDto
+ {
+ Id = message.Id,
+ TaskId = message.TaskId,
+ SenderUserId = message.SenderUserId,
+ SenderName = senderName,
+ MessageType = message.MessageType.ToString(),
+ TextContent = message.TextContent,
+ IsPinned = message.IsPinned,
+ PinnedDate = message.PinnedDate,
+ PinnedByUserId = message.PinnedByUserId,
+ CreationDate = message.CreationDate,
+ IsMine = message.SenderUserId == currentUserId
+ };
+
+ if (message.FileId.HasValue)
+ {
+ var file = await _context.UploadedFiles.FirstOrDefaultAsync(f => f.Id == message.FileId.Value, cancellationToken);
+ if (file != null)
+ {
+ dto.File = new MessageFileDto
+ {
+ Id = file.Id,
+ FileName = file.OriginalFileName,
+ FileUrl = file.StorageUrl ?? "",
+ FileSizeBytes = file.FileSizeBytes,
+ FileType = file.FileType.ToString(),
+ ThumbnailUrl = file.ThumbnailUrl
+ };
+ }
+ }
+
+ messageDtos.Add(dto);
+ }
+
+ return OperationResult>.Success(messageDtos);
+ }
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/SearchMessages/SearchMessagesQuery.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/SearchMessages/SearchMessagesQuery.cs
new file mode 100644
index 00000000..dffc1331
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/SearchMessages/SearchMessagesQuery.cs
@@ -0,0 +1,72 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.SearchMessages;
+
+public record SearchMessagesQuery(
+ Guid TaskId,
+ string SearchText,
+ int Page = 1,
+ int PageSize = 20
+) : IBaseQuery>;
+
+public class SearchMessagesQueryHandler : IBaseQueryHandler>
+{
+ private readonly IProgramManagerDbContext _context;
+ private readonly IAuthHelper _authHelper;
+
+ public SearchMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper)
+ {
+ _context = context;
+ _authHelper = authHelper;
+ }
+
+ public async Task>> Handle(SearchMessagesQuery request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId();
+ var skip = (request.Page - 1) * request.PageSize;
+
+ var messages = await _context.TaskChatMessages
+ .Where(m => m.TaskId == request.TaskId &&
+ m.TextContent != null &&
+ m.TextContent.Contains(request.SearchText) &&
+ !m.IsDeleted)
+ .Include(m => m.ReplyToMessage)
+ .OrderByDescending(m => m.CreationDate)
+ .Skip(skip)
+ .Take(request.PageSize)
+ .ToListAsync(cancellationToken);
+
+ // ✅ گرفتن تمامی کاربران برای نمایش نام کامل فرستنده
+ var senderUserIds = messages.Select(m => m.SenderUserId).Distinct().ToList();
+ var users = await _context.Users
+ .Where(u => senderUserIds.Contains(u.Id))
+ .ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
+
+ var messageDtos = new List();
+ foreach (var message in messages)
+ {
+ // ✅ نام فرستنده را از User واقعی بگیر
+ var senderName = users.GetValueOrDefault(message.SenderUserId, "کاربر ناشناس");
+
+ var dto = new MessageDto
+ {
+ Id = message.Id,
+ TaskId = message.TaskId,
+ SenderUserId = message.SenderUserId,
+ SenderName = senderName,
+ MessageType = message.MessageType.ToString(),
+ TextContent = message.TextContent,
+ CreationDate = message.CreationDate,
+ IsMine = message.SenderUserId == currentUserId
+ };
+
+ messageDtos.Add(dto);
+ }
+
+ return OperationResult>.Success(messageDtos);
+ }
+}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IFileStorageService.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IFileStorageService.cs
new file mode 100644
index 00000000..3a429f63
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IFileStorageService.cs
@@ -0,0 +1,38 @@
+using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
+
+namespace GozareshgirProgramManager.Application.Services.FileManagement;
+
+///
+/// سرویس ذخیرهسازی فایل
+///
+public interface IFileStorageService
+{
+ ///
+ /// آپلود فایل
+ ///
+ Task<(string StoragePath, string StorageUrl)> UploadAsync(
+ Stream fileStream,
+ string uniqueFileName,
+ string category);
+
+ ///
+ /// حذف فایل
+ ///
+ Task DeleteAsync(string storagePath);
+
+ ///
+ /// دریافت فایل
+ ///
+ Task GetFileStreamAsync(string storagePath);
+
+ ///
+ /// بررسی وجود فایل
+ ///
+ Task ExistsAsync(string storagePath);
+
+ ///
+ /// دریافت URL فایل
+ ///
+ string GetFileUrl(string storagePath);
+}
+
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IThumbnailGeneratorService.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IThumbnailGeneratorService.cs
new file mode 100644
index 00000000..cff21a1a
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IThumbnailGeneratorService.cs
@@ -0,0 +1,34 @@
+namespace GozareshgirProgramManager.Application.Services.FileManagement;
+
+///
+/// سرویس تولید thumbnail برای تصاویر و ویدیوها
+///
+public interface IThumbnailGeneratorService
+{
+ ///
+ /// تولید thumbnail برای تصویر
+ ///
+ Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync(
+ string imagePath,
+ string category,
+ int width = 200,
+ int height = 200);
+
+ ///
+ /// تولید thumbnail برای ویدیو
+ ///
+ Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(
+ string videoPath,
+ string category);
+
+ ///
+ /// حذف thumbnail
+ ///
+ Task DeleteThumbnailAsync(string thumbnailPath);
+
+ ///
+ /// دریافت ابعاد تصویر
+ ///
+ Task<(int Width, int Height)?> GetImageDimensionsAsync(string imagePath);
+}
+
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Interfaces/IProgramManagerDbContext.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Interfaces/IProgramManagerDbContext.cs
index 1a477bec..a8f8400b 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Interfaces/IProgramManagerDbContext.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Interfaces/IProgramManagerDbContext.cs
@@ -7,6 +7,8 @@ using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities;
using GozareshgirProgramManager.Domain.SkillAgg.Entities;
using GozareshgirProgramManager.Domain.UserAgg.Entities;
using Microsoft.EntityFrameworkCore;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
+using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
namespace GozareshgirProgramManager.Application._Common.Interfaces;
@@ -26,6 +28,9 @@ public interface IProgramManagerDbContext
DbSet ProjectTasks { get; set; }
+ DbSet TaskChatMessages { get; set; }
+ DbSet UploadedFiles { get; set; }
+
DbSet Skills { get; set; }
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Entities/UploadedFile.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Entities/UploadedFile.cs
new file mode 100644
index 00000000..b256fd93
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Entities/UploadedFile.cs
@@ -0,0 +1,244 @@
+using GozareshgirProgramManager.Domain._Common;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
+using GozareshgirProgramManager.Domain.FileManagementAgg.Events;
+using FileType = GozareshgirProgramManager.Domain.FileManagementAgg.Enums.FileType;
+
+namespace GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
+
+///
+/// فایل آپلود شده - Aggregate Root
+/// مدیریت مرکزی تمام فایلهای سیستم
+///
+public class UploadedFile : EntityBase
+{
+ private UploadedFile()
+ {
+ }
+
+ public UploadedFile(
+ string originalFileName,
+ long fileSizeBytes,
+ string mimeType,
+ FileType fileType,
+ FileCategory category,
+ long uploadedByUserId,
+ StorageProvider storageProvider = StorageProvider.LocalFileSystem)
+ {
+ OriginalFileName = originalFileName;
+ FileSizeBytes = fileSizeBytes;
+ MimeType = mimeType;
+ FileType = fileType;
+ Category = category;
+ UploadedByUserId = uploadedByUserId;
+ UploadDate = DateTime.Now;
+ StorageProvider = storageProvider;
+ Status = FileStatus.Uploading;
+
+ // Generate unique file name
+ FileExtension = Path.GetExtension(originalFileName);
+ UniqueFileName = $"{Guid.NewGuid()}{FileExtension}";
+
+ ValidateFile();
+ AddDomainEvent(new FileUploadStartedEvent(Id, originalFileName, uploadedByUserId));
+ }
+
+ // اطلاعات فایل
+ public string OriginalFileName { get; private set; } = string.Empty;
+ public string UniqueFileName { get; private set; } = string.Empty;
+ public string FileExtension { get; private set; } = string.Empty;
+ public long FileSizeBytes { get; private set; }
+ public string MimeType { get; private set; } = string.Empty;
+ public FileType FileType { get; private set; }
+ public FileCategory Category { get; private set; }
+
+ // ذخیرهسازی
+ public StorageProvider StorageProvider { get; private set; }
+ public string? StoragePath { get; private set; }
+ public string? StorageUrl { get; private set; }
+ public string? ThumbnailUrl { get; private set; }
+
+ // متادیتا
+ public long UploadedByUserId { get; private set; }
+ public DateTime UploadDate { get; private set; }
+ public FileStatus Status { get; private set; }
+
+ // اطلاعات تصویر (اختیاری - برای Image)
+ public int? ImageWidth { get; private set; }
+ public int? ImageHeight { get; private set; }
+
+ // اطلاعات صوت/ویدیو (اختیاری)
+ public int? DurationSeconds { get; private set; }
+
+ // امنیت
+ public DateTime? VirusScanDate { get; private set; }
+ public bool? IsVirusScanPassed { get; private set; }
+ public string? VirusScanResult { get; private set; }
+
+ // Soft Delete
+ public bool IsDeleted { get; private set; }
+ public DateTime? DeletedDate { get; private set; }
+ public long? DeletedByUserId { get; private set; }
+
+ // Reference tracking (چه entityهایی از این فایل استفاده میکنند)
+ public string? ReferenceEntityType { get; private set; }
+ public string? ReferenceEntityId { get; private set; }
+
+ private void ValidateFile()
+ {
+ if (string.IsNullOrWhiteSpace(OriginalFileName))
+ {
+ throw new BadRequestException("نام فایل نمیتواند خالی باشد");
+ }
+
+ if (FileSizeBytes <= 0)
+ {
+ throw new BadRequestException("حجم فایل باید بیشتر از صفر باشد");
+ }
+
+ if (string.IsNullOrWhiteSpace(MimeType))
+ {
+ throw new BadRequestException("نوع MIME فایل باید مشخص شود");
+ }
+
+ // محدودیت حجم (مثلاً 100MB)
+ const long maxSizeBytes = 100 * 1024 * 1024; // 100MB
+ if (FileSizeBytes > maxSizeBytes)
+ {
+ throw new BadRequestException($"حجم فایل نباید بیشتر از {maxSizeBytes / (1024 * 1024)} مگابایت باشد");
+ }
+ }
+
+ public void CompleteUpload(string storagePath, string storageUrl)
+ {
+ if (Status != FileStatus.Uploading)
+ {
+ throw new BadRequestException("فایل قبلاً آپلود شده است");
+ }
+
+ if (string.IsNullOrWhiteSpace(storagePath))
+ {
+ throw new BadRequestException("مسیر ذخیرهسازی نمیتواند خالی باشد");
+ }
+
+ if (string.IsNullOrWhiteSpace(storageUrl))
+ {
+ throw new BadRequestException("URL فایل نمیتواند خالی باشد");
+ }
+
+ StoragePath = storagePath;
+ StorageUrl = storageUrl;
+ Status = FileStatus.Active;
+
+ AddDomainEvent(new FileUploadCompletedEvent(Id, OriginalFileName, StorageUrl, UploadedByUserId));
+ }
+
+ public void SetThumbnail(string thumbnailUrl)
+ {
+ if (FileType != FileType.Image && FileType != FileType.Video)
+ {
+ throw new BadRequestException("فقط میتوان برای تصاویر و ویدیوها thumbnail تنظیم کرد");
+ }
+
+ ThumbnailUrl = thumbnailUrl;
+ }
+
+ public void SetImageDimensions(int width, int height)
+ {
+ if (FileType != FileType.Image)
+ {
+ throw new BadRequestException("فقط میتوان برای تصاویر ابعاد تنظیم کرد");
+ }
+
+ if (width <= 0 || height <= 0)
+ {
+ throw new BadRequestException("ابعاد تصویر باید بیشتر از صفر باشد");
+ }
+
+ ImageWidth = width;
+ ImageHeight = height;
+ }
+
+ public void SetDuration(int durationSeconds)
+ {
+ if (FileType != FileType.Audio && FileType != FileType.Video)
+ {
+ throw new BadRequestException("فقط میتوان برای فایلهای صوتی و تصویری مدت زمان تنظیم کرد");
+ }
+
+ if (durationSeconds <= 0)
+ {
+ throw new BadRequestException("مدت زمان باید بیشتر از صفر باشد");
+ }
+
+ DurationSeconds = durationSeconds;
+ }
+
+ public void MarkAsDeleted(long deletedByUserId)
+ {
+ if (IsDeleted)
+ {
+ throw new BadRequestException("فایل قبلاً حذف شده است");
+ }
+
+ IsDeleted = true;
+ DeletedDate = DateTime.Now;
+ DeletedByUserId = deletedByUserId;
+ Status = FileStatus.Deleted;
+
+ AddDomainEvent(new FileDeletedEvent(Id, OriginalFileName, deletedByUserId));
+ }
+
+ public void SetReference(string entityType, string entityId)
+ {
+ ReferenceEntityType = entityType;
+ ReferenceEntityId = entityId;
+ }
+
+ public bool IsImage()
+ {
+ return FileType == FileType.Image;
+ }
+
+ public bool IsVideo()
+ {
+ return FileType == FileType.Video;
+ }
+
+ public bool IsAudio()
+ {
+ return FileType == FileType.Audio;
+ }
+
+ public bool IsDocument()
+ {
+ return FileType == FileType.Document;
+ }
+
+ public bool IsUploadedBy(long userId)
+ {
+ return UploadedByUserId == userId;
+ }
+
+ public bool IsActive()
+ {
+ return Status == FileStatus.Active && !IsDeleted;
+ }
+
+ public string GetFileSizeFormatted()
+ {
+ const long kb = 1024;
+ const long mb = kb * 1024;
+ const long gb = mb * 1024;
+
+ if (FileSizeBytes >= gb)
+ return $"{FileSizeBytes / (double)gb:F2} GB";
+ if (FileSizeBytes >= mb)
+ return $"{FileSizeBytes / (double)mb:F2} MB";
+ if (FileSizeBytes >= kb)
+ return $"{FileSizeBytes / (double)kb:F2} KB";
+
+ return $"{FileSizeBytes} Bytes";
+ }
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileCategory.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileCategory.cs
new file mode 100644
index 00000000..25e7c3f6
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileCategory.cs
@@ -0,0 +1,15 @@
+namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
+
+///
+/// دستهبندی فایل - مشخص میکند فایل در کجا استفاده شده
+///
+public enum FileCategory
+{
+ TaskChatMessage = 1, // پیام چت تسک
+ TaskAttachment = 2, // ضمیمه تسک
+ ProjectDocument = 3, // مستندات پروژه
+ UserProfilePhoto = 4, // عکس پروفایل کاربر
+ Report = 5, // گزارش
+ Other = 6 // سایر
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileStatus.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileStatus.cs
new file mode 100644
index 00000000..5781e1f7
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileStatus.cs
@@ -0,0 +1,13 @@
+namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
+
+///
+/// وضعیت فایل
+///
+public enum FileStatus
+{
+ Uploading = 1, // در حال آپلود
+ Active = 2, // فعال و قابل استفاده
+ Deleted = 5, // حذف شده (Soft Delete)
+ Archived = 6 // آرشیو شده
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileType.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileType.cs
new file mode 100644
index 00000000..63d07122
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileType.cs
@@ -0,0 +1,15 @@
+namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
+
+///
+/// نوع فایل
+///
+public enum FileType
+{
+ Document = 1, // اسناد (PDF, Word, Excel, etc.)
+ Image = 2, // تصویر
+ Video = 3, // ویدیو
+ Audio = 4, // صوت
+ Archive = 5, // فایل فشرده (ZIP, RAR)
+ Other = 6 // سایر
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/StorageProvider.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/StorageProvider.cs
new file mode 100644
index 00000000..a8813a56
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/StorageProvider.cs
@@ -0,0 +1,10 @@
+namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
+
+///
+/// نوع ذخیرهساز فایل
+///
+public enum StorageProvider
+{
+ LocalFileSystem = 1, // دیسک محلی سرور
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Events/FileEvents.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Events/FileEvents.cs
new file mode 100644
index 00000000..df94f2df
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Events/FileEvents.cs
@@ -0,0 +1,36 @@
+using GozareshgirProgramManager.Domain._Common;
+
+namespace GozareshgirProgramManager.Domain.FileManagementAgg.Events;
+
+// File Upload Events
+public record FileUploadStartedEvent(Guid FileId, string FileName, long UploadedByUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record FileUploadCompletedEvent(Guid FileId, string FileName, string StorageUrl, long UploadedByUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record FileDeletedEvent(Guid FileId, string FileName, long DeletedByUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+// Virus Scan Events
+public record FileQuarantinedEvent(Guid FileId, string FileName) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record FileVirusScanPassedEvent(Guid FileId, string FileName) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record FileInfectedEvent(Guid FileId, string FileName, string ScanResult) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Repositories/IUploadedFileRepository.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Repositories/IUploadedFileRepository.cs
new file mode 100644
index 00000000..b56afa70
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Repositories/IUploadedFileRepository.cs
@@ -0,0 +1,91 @@
+using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
+using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
+
+namespace GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
+
+///
+/// Repository برای مدیریت فایلهای آپلود شده
+///
+public interface IUploadedFileRepository
+{
+ ///
+ /// دریافت فایل بر اساس شناسه
+ ///
+ Task GetByIdAsync(Guid fileId);
+
+ ///
+ /// دریافت فایل بر اساس نام یکتا
+ ///
+ Task GetByUniqueFileNameAsync(string uniqueFileName);
+
+ ///
+ /// دریافت لیست فایلهای یک کاربر
+ ///
+ Task> GetUserFilesAsync(long userId, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت فایلهای یک دسته خاص
+ ///
+ Task> GetByCategoryAsync(FileCategory category, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت فایلهای با وضعیت خاص
+ ///
+ Task> GetByStatusAsync(FileStatus status, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت فایلهای یک Reference خاص
+ ///
+ Task> GetByReferenceAsync(string entityType, string entityId);
+
+ ///
+ /// جستجو در فایلها بر اساس نام
+ ///
+ Task> SearchByNameAsync(string searchTerm, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت تعداد کل فایلهای یک کاربر
+ ///
+ Task GetUserFilesCountAsync(long userId);
+
+ ///
+ /// دریافت مجموع حجم فایلهای یک کاربر (به بایت)
+ ///
+ Task GetUserTotalFileSizeAsync(long userId);
+
+ ///
+ /// دریافت فایلهای منقضی شده برای پاکسازی
+ ///
+ Task> GetExpiredFilesAsync(DateTime olderThan);
+
+ ///
+ /// اضافه کردن فایل جدید
+ ///
+ Task AddAsync(UploadedFile file);
+
+ ///
+ /// بهروزرسانی فایل
+ ///
+ Task UpdateAsync(UploadedFile file);
+
+ ///
+ /// حذف فیزیکی فایل (فقط برای cleanup)
+ ///
+ Task DeleteAsync(UploadedFile file);
+
+ ///
+ /// ذخیره تغییرات
+ ///
+ Task SaveChangesAsync();
+
+ ///
+ /// بررسی وجود فایل
+ ///
+ Task ExistsAsync(Guid fileId);
+
+ ///
+ /// بررسی وجود فایل با نام یکتا
+ ///
+ Task ExistsByUniqueFileNameAsync(string uniqueFileName);
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/Project.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/Project.cs
index dc2bfa15..8e8e2b31 100644
--- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/Project.cs
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/Project.cs
@@ -74,6 +74,15 @@ public class Project : ProjectHierarchyNode
}
}
+ public void RemoveProjectSection(Guid skillId)
+ {
+ var section = _projectSections.FirstOrDefault(s => s.SkillId == skillId);
+ if (section != null)
+ {
+ _projectSections.Remove(section);
+ }
+ }
+
public void ClearProjectSections()
{
_projectSections.Clear();
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectPhase.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectPhase.cs
index 3534c50f..60b98df9 100644
--- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectPhase.cs
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectPhase.cs
@@ -87,6 +87,15 @@ public class ProjectPhase : ProjectHierarchyNode
}
}
+ public void RemovePhaseSection(Guid skillId)
+ {
+ var section = _phaseSections.FirstOrDefault(s => s.SkillId == skillId);
+ if (section != null)
+ {
+ _phaseSections.Remove(section);
+ }
+ }
+
public void ClearPhaseSections()
{
_phaseSections.Clear();
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectTask.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectTask.cs
index 863e332f..286d0387 100644
--- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectTask.cs
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectTask.cs
@@ -20,7 +20,7 @@ public class ProjectTask : ProjectHierarchyNode
{
PhaseId = phaseId;
_sections = new List();
- Priority = TaskPriority.Medium;
+ Priority = ProjectTaskPriority.Medium;
AddDomainEvent(new TaskCreatedEvent(Id, phaseId, name));
}
@@ -30,7 +30,7 @@ public class ProjectTask : ProjectHierarchyNode
// Task-specific properties
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? EndDate { get; private set; }
public DateTime? DueDate { get; private set; }
@@ -119,7 +119,7 @@ public class ProjectTask : ProjectHierarchyNode
AddDomainEvent(new TaskStatusUpdatedEvent(Id, status));
}
- public void SetPriority(TaskPriority priority)
+ public void SetPriority(ProjectTaskPriority priority)
{
Priority = priority;
AddDomainEvent(new TaskPriorityUpdatedEvent(Id, priority));
@@ -246,4 +246,9 @@ public class ProjectTask : ProjectHierarchyNode
}
#endregion
+
+ public void ClearTaskSections()
+ {
+ _sections.Clear();
+ }
}
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs
index 7209dd00..83b86c23 100644
--- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs
@@ -157,6 +157,27 @@ public class TaskSection : EntityBase
return TimeSpan.FromTicks(_activities.Sum(a => a.GetTimeSpent().Ticks));
}
+ ///
+ /// محاسبه درصد پیشرفت بر اساس زمان مصرف شده به تایم برآورد شده
+ /// اگر وضعیت Completed باشد، همیشه 100 درصد برمیگرداند
+ ///
+ 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()
{
return Status == TaskSectionStatus.Completed;
@@ -249,7 +270,7 @@ public class TaskSection : EntityBase
// متوقف کردن فعالیت با EndDate دقیق شده
activeActivity.StopWorkWithSpecificTime(adjustedEndDate, "متوقف خودکار - بیش از تایم تعیین شده");
- UpdateStatus(TaskSectionStatus.Incomplete);
+ UpdateStatus(TaskSectionStatus.PendingForCompletion);
}
}
}
\ No newline at end of file
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/AssignmentStatus.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/AssignmentStatus.cs
new file mode 100644
index 00000000..91cbeffd
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/AssignmentStatus.cs
@@ -0,0 +1,23 @@
+namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
+
+///
+/// وضعیت تکلیف دهی برای بخشهای مختلف پروژه
+///
+public enum AssignmentStatus
+{
+ ///
+ /// تعیین تکلیف نشده
+ ///
+ Unassigned = 0,
+
+ ///
+ /// تعیین تکلیف شده
+ ///
+ Assigned = 1,
+
+ ///
+ /// فقط کاربر تعیین شده
+ ///
+ UserOnly = 2,
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/TaskPriority.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/ProjectTaskPriority.cs
similarity index 92%
rename from ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/TaskPriority.cs
rename to ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/ProjectTaskPriority.cs
index 13ccd784..2a2ab36f 100644
--- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/TaskPriority.cs
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/ProjectTaskPriority.cs
@@ -3,7 +3,7 @@ namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
///
/// اولویت تسک
///
-public enum TaskPriority
+public enum ProjectTaskPriority
{
///
/// پایین
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Events/ProjectEvents.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Events/ProjectEvents.cs
index 155fec66..64f21ef9 100644
--- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Events/ProjectEvents.cs
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Events/ProjectEvents.cs
@@ -78,7 +78,7 @@ public record TaskStatusUpdatedEvent(Guid TaskId, TaskStatus Status) : IDomainEv
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;
}
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/IProjectTaskRepository.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/IProjectTaskRepository.cs
index 8b0ff688..9894a764 100644
--- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/IProjectTaskRepository.cs
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/IProjectTaskRepository.cs
@@ -36,7 +36,7 @@ public interface IProjectTaskRepository : IRepository
///
/// Get tasks by priority
///
- Task> GetByPriorityAsync(ProjectAgg.Enums.TaskPriority priority);
+ Task> GetByPriorityAsync(ProjectAgg.Enums.ProjectTaskPriority priority);
///
/// Get tasks assigned to user
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/ITaskSectionRepository.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/ITaskSectionRepository.cs
index b4a4c5dd..50e1a2b0 100644
--- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/ITaskSectionRepository.cs
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/ITaskSectionRepository.cs
@@ -13,4 +13,8 @@ public interface ITaskSectionRepository: IRepository
Task> GetAssignedToUserAsync(long userId);
Task> GetActiveSectionsIncludeAllAsync(CancellationToken cancellationToken);
+ Task HasUserAnyInProgressSectionAsync(long userId, CancellationToken cancellationToken = default);
+
+ // جدید: دریافت سکشنهایی که هنوز Completed یا PendingForCompletion نشدهاند با اطلاعات کامل
+ Task> GetAllNotCompletedOrPendingIncludeAllAsync(CancellationToken cancellationToken);
}
\ No newline at end of file
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs
new file mode 100644
index 00000000..c937e781
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs
@@ -0,0 +1,183 @@
+using GozareshgirProgramManager.Domain._Common;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Events;
+using MessageType = GozareshgirProgramManager.Domain.TaskChatAgg.Enums.MessageType;
+
+namespace GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
+
+///
+/// پیام چت تسک - Aggregate Root
+/// هر کسی که به تسک دسترسی داشته باشد میتواند پیامها را ببیند و ارسال کند
+/// نیازی به مدیریت گروه و ممبر نیست چون دسترسی از طریق خود تسک کنترل میشود
+///
+public class TaskChatMessage : EntityBase
+{
+ private TaskChatMessage()
+ {
+ }
+
+ public TaskChatMessage(Guid taskId, long senderUserId, MessageType messageType,
+ string? textContent = null,Guid? fileId = null)
+ {
+ TaskId = taskId;
+ SenderUserId = senderUserId;
+ MessageType = messageType;
+ TextContent = textContent;
+ IsEdited = false;
+ IsDeleted = false;
+ IsPinned = false;
+ if (fileId.HasValue)
+ {
+ SetFile(fileId.Value);
+ }
+ ValidateMessage();
+ AddDomainEvent(new TaskChatMessageSentEvent(Id, taskId, senderUserId, messageType));
+ }
+
+ // Reference به Task (Foreign Key فقط - بدون Navigation Property برای جلوگیری از coupling)
+ public Guid TaskId { get; private set; }
+
+ public long SenderUserId { get; private set; }
+
+ public MessageType MessageType { get; private set; }
+
+ // محتوای متنی (برای پیامهای Text و Caption برای فایل/تصویر)
+ public string? TextContent { get; private set; }
+
+ // ارجاع به فایل (برای پیامهای File, Voice, Image, Video)
+ public Guid? FileId { get; private set; }
+
+ // پیام Reply
+ public Guid? ReplyToMessageId { get; private set; }
+ public TaskChatMessage? ReplyToMessage { get; private set; }
+
+ // وضعیت پیام
+ public bool IsEdited { get; private set; }
+ public DateTime? EditedDate { get; private set; }
+ public bool IsDeleted { get; private set; }
+ public DateTime? DeletedDate { get; private set; }
+ public bool IsPinned { get; private set; }
+ public DateTime? PinnedDate { get; private set; }
+ public long? PinnedByUserId { get; private set; }
+
+ private void ValidateMessage()
+ {
+ // ✅ بررسی پیامهای متنی
+ if (MessageType == MessageType.Text && string.IsNullOrWhiteSpace(TextContent))
+ {
+ throw new BadRequestException("پیام متنی نمیتواند خالی باشد");
+ }
+
+ // ✅ بررسی پیامهای فایلی - باید FileId داشته باشند
+ if ((MessageType == MessageType.File || MessageType == MessageType.Voice ||
+ MessageType == MessageType.Image || MessageType == MessageType.Video)
+ && FileId == null)
+ {
+ throw new BadRequestException("پیامهای فایلی باید شناسه فایل داشته باشند");
+ }
+
+ // ✅ بررسی یادداشتهای سیستم - باید محتوای متنی داشته باشند
+ if (MessageType == MessageType.Note && string.IsNullOrWhiteSpace(TextContent))
+ {
+ throw new BadRequestException("یادداشت نمیتواند خالی باشد");
+ }
+ }
+
+ public void SetFile(Guid fileId)
+ {
+ if (MessageType != MessageType.File && MessageType != MessageType.Image &&
+ MessageType != MessageType.Video && MessageType != MessageType.Voice)
+ {
+ throw new BadRequestException("فقط میتوان برای پیامهای فایل، تصویر، ویدیو و صدا شناسه فایل تنظیم کرد");
+ }
+
+ FileId = fileId;
+ }
+
+ public void EditMessage(string newTextContent, long editorUserId)
+ {
+ if (IsDeleted)
+ {
+ throw new BadRequestException("نمیتوان پیام حذف شده را ویرایش کرد");
+ }
+
+ if (editorUserId != SenderUserId)
+ {
+ throw new BadRequestException("فقط فرستنده میتواند پیام را ویرایش کند");
+ }
+
+ if ((MessageType != MessageType.Text && !string.IsNullOrWhiteSpace(TextContent)))
+ {
+ throw new BadRequestException("فقط پیامهای متنی قابل ویرایش هستند");
+ }
+
+ if (string.IsNullOrWhiteSpace(newTextContent))
+ {
+ throw new BadRequestException("محتوای پیام نمیتواند خالی باشد");
+ }
+
+ TextContent = newTextContent;
+ IsEdited = true;
+ EditedDate = DateTime.Now;
+ AddDomainEvent(new TaskChatMessageEditedEvent(Id, TaskId, editorUserId));
+ }
+
+ public void DeleteMessage(long deleterUserId)
+ {
+ if (IsDeleted)
+ {
+ throw new BadRequestException("پیام قبلاً حذف شده است");
+ }
+
+ if (deleterUserId != SenderUserId)
+ {
+ throw new BadRequestException("فقط فرستنده میتواند پیام را حذف کند");
+ }
+
+ IsDeleted = true;
+ DeletedDate = DateTime.Now;
+ AddDomainEvent(new TaskChatMessageDeletedEvent(Id, TaskId, deleterUserId));
+ }
+
+ public void PinMessage(long pinnerUserId)
+ {
+ if (IsDeleted)
+ {
+ throw new BadRequestException("نمیتوان پیام حذف شده را پین کرد");
+ }
+
+ if (IsPinned)
+ {
+ throw new BadRequestException("این پیام قبلاً پین شده است");
+ }
+
+ IsPinned = true;
+ PinnedDate = DateTime.Now;
+ PinnedByUserId = pinnerUserId;
+ AddDomainEvent(new TaskChatMessagePinnedEvent(Id, TaskId, pinnerUserId));
+ }
+
+ public void UnpinMessage(long unpinnerUserId)
+ {
+ if (!IsPinned)
+ {
+ throw new BadRequestException("این پیام پین نشده است");
+ }
+
+ IsPinned = false;
+ PinnedDate = null;
+ PinnedByUserId = null;
+ AddDomainEvent(new TaskChatMessageUnpinnedEvent(Id, TaskId, unpinnerUserId));
+ }
+
+ public void SetReplyTo(Guid replyToMessageId)
+ {
+ ReplyToMessageId = replyToMessageId;
+ }
+
+ public bool IsSentBy(long userId)
+ {
+ return SenderUserId == userId;
+ }
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Enums/MessageType.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Enums/MessageType.cs
new file mode 100644
index 00000000..f35c1034
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Enums/MessageType.cs
@@ -0,0 +1,15 @@
+namespace GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
+
+///
+/// نوع پیام در چت تسک
+///
+public enum MessageType
+{
+ Text = 1, // پیام متنی
+ File = 2, // فایل (اسناد، PDF، و غیره)
+ Image = 3, // تصویر
+ Voice = 4, // پیام صوتی
+ Video = 5, // ویدیو
+ Note = 6, // ✅ یادداشت سیستم (برای زمان اضافی و اطلاعات خودکار)
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Events/TaskChatEvents.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Events/TaskChatEvents.cs
new file mode 100644
index 00000000..9493fa55
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Events/TaskChatEvents.cs
@@ -0,0 +1,31 @@
+using GozareshgirProgramManager.Domain._Common;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
+
+namespace GozareshgirProgramManager.Domain.TaskChatAgg.Events;
+
+// Message Events
+public record TaskChatMessageSentEvent(Guid MessageId, Guid TaskId, long SenderUserId, MessageType MessageType) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record TaskChatMessageEditedEvent(Guid MessageId, Guid TaskId, long EditorUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record TaskChatMessageDeletedEvent(Guid MessageId, Guid TaskId, long DeleterUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record TaskChatMessagePinnedEvent(Guid MessageId, Guid TaskId, long PinnerUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record TaskChatMessageUnpinnedEvent(Guid MessageId, Guid TaskId, long UnpinnerUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs
new file mode 100644
index 00000000..141ebaf1
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs
@@ -0,0 +1,75 @@
+using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
+
+namespace GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
+
+///
+/// Repository برای مدیریت پیامهای چت تسک
+///
+public interface ITaskChatMessageRepository
+{
+ ///
+ /// دریافت پیام بر اساس شناسه
+ ///
+ Task GetByIdAsync(Guid messageId);
+
+ ///
+ /// دریافت لیست پیامهای یک تسک (با صفحهبندی)
+ ///
+ Task> GetTaskMessagesAsync(Guid taskId, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت تعداد کل پیامهای یک تسک
+ ///
+ Task GetTaskMessageCountAsync(Guid taskId);
+
+ ///
+ /// دریافت پیامهای پین شده یک تسک
+ ///
+ Task> GetPinnedMessagesAsync(Guid taskId);
+
+ ///
+ /// دریافت آخرین پیام یک تسک
+ ///
+ Task GetLastMessageAsync(Guid taskId);
+
+ ///
+ /// جستجو در پیامهای یک تسک
+ ///
+ Task> SearchMessagesAsync(Guid taskId, string searchText, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت پیامهای یک کاربر خاص در یک تسک
+ ///
+ Task> GetUserMessagesAsync(Guid taskId, long userId, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت پیامهای با فایل (تصویر، ویدیو، فایل و...) - پیامهایی که FileId دارند
+ ///
+ Task> GetMediaMessagesAsync(Guid taskId, int pageNumber, int pageSize);
+
+ ///
+ /// اضافه کردن پیام جدید
+ ///
+ Task AddAsync(TaskChatMessage message);
+
+ ///
+ /// بهروزرسانی پیام
+ ///
+ Task UpdateAsync(TaskChatMessage message);
+
+ ///
+ /// حذف فیزیکی پیام (در صورت نیاز - معمولاً استفاده نمیشود)
+ ///
+ Task DeleteAsync(TaskChatMessage message);
+
+ ///
+ /// ذخیره تغییرات
+ ///
+ Task SaveChangesAsync();
+
+ ///
+ /// بررسی وجود پیام
+ ///
+ Task ExistsAsync(Guid messageId);
+}
+
diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs
index 1543fb97..20618330 100644
--- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs
+++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs
@@ -4,9 +4,11 @@
using FluentValidation;
using GozareshgirProgramManager.Application._Common.Behaviors;
using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application.Services.FileManagement;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.CheckoutAgg.Repositories;
using GozareshgirProgramManager.Domain.CustomerAgg.Repositories;
+using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
using GozareshgirProgramManager.Domain.RoleAgg.Repositories;
using GozareshgirProgramManager.Domain.RoleAgg.Repositories;
@@ -14,6 +16,7 @@ using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Repositories;
using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Repositories;
using GozareshgirProgramManager.Domain.SkillAgg.Repositories;
using GozareshgirProgramManager.Domain.SkillAgg.Repositories;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
using GozareshgirProgramManager.Domain.UserAgg.Repositories;
using GozareshgirProgramManager.Infrastructure.Persistence;
using GozareshgirProgramManager.Infrastructure.Persistence.Context;
@@ -82,6 +85,14 @@ public static class DependencyInjection
services.AddScoped();
+ // File Management & Task Chat
+ services.AddScoped();
+ services.AddScoped();
+
+ // File Storage Services
+ services.AddScoped();
+ services.AddScoped();
+
// JWT Settings
services.Configure(configuration.GetSection("JwtSettings"));
diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/GozareshgirProgramManager.Infrastructure.csproj b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/GozareshgirProgramManager.Infrastructure.csproj
index 63dd5c8a..95f55437 100644
--- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/GozareshgirProgramManager.Infrastructure.csproj
+++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/GozareshgirProgramManager.Infrastructure.csproj
@@ -15,11 +15,21 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.AspNetCore.Hosting.Abstractions.dll
+
+
+ C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.Extensions.Hosting.Abstractions.dll
+
+
diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/20260105112925_add task chat - uploaded file.Designer.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/20260105112925_add task chat - uploaded file.Designer.cs
new file mode 100644
index 00000000..aaefdec1
--- /dev/null
+++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/20260105112925_add task chat - uploaded file.Designer.cs
@@ -0,0 +1,1075 @@
+//
+using System;
+using GozareshgirProgramManager.Infrastructure.Persistence.Context;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace GozareshgirProgramManager.Infrastructure.Migrations
+{
+ [DbContext(typeof(ProgramManagerDbContext))]
+ [Migration("20260105112925_add task chat - uploaded file")]
+ partial class addtaskchatuploadedfile
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("GozareshgirProgramManager.Domain.CheckoutAgg.Entities.Checkout", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CheckoutEndDate")
+ .HasColumnType("datetime2");
+
+ b.Property("CheckoutStartDate")
+ .HasColumnType("datetime2");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime2");
+
+ b.Property("DeductionFromSalary")
+ .HasColumnType("float");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("MandatoryHours")
+ .HasColumnType("int");
+
+ b.Property("Month")
+ .HasColumnType("int");
+
+ b.Property("MonthlySalaryDefined")
+ .HasColumnType("float");
+
+ b.Property("MonthlySalaryPay")
+ .HasColumnType("float");
+
+ b.Property("RemainingHours")
+ .HasColumnType("int");
+
+ b.Property("TotalDaysWorked")
+ .HasColumnType("int");
+
+ b.Property("TotalHoursWorked")
+ .HasColumnType("int");
+
+ b.Property