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("UserId") + .HasColumnType("bigint"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Checkouts", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.CustomerAgg.Customer", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.FileManagementAgg.Entities.UploadedFile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DeletedByUserId") + .HasColumnType("bigint"); + + b.Property("DeletedDate") + .HasColumnType("datetime2"); + + b.Property("DurationSeconds") + .HasColumnType("int"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ImageHeight") + .HasColumnType("int"); + + b.Property("ImageWidth") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsVirusScanPassed") + .HasColumnType("bit"); + + b.Property("MimeType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ReferenceEntityId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ReferenceEntityType") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("StoragePath") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("StorageProvider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("StorageUrl") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ThumbnailUrl") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("UniqueFileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UploadDate") + .HasColumnType("datetime2"); + + b.Property("UploadedByUserId") + .HasColumnType("bigint"); + + b.Property("VirusScanDate") + .HasColumnType("datetime2"); + + b.Property("VirusScanResult") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("Status"); + + b.HasIndex("UniqueFileName") + .IsUnique(); + + b.HasIndex("UploadedByUserId"); + + b.HasIndex("ReferenceEntityType", "ReferenceEntityId"); + + b.ToTable("UploadedFiles", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.PhaseSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("PhaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("SkillId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PhaseId"); + + b.HasIndex("SkillId"); + + b.ToTable("PhaseSections"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("HasAssignmentOverride") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PlannedEndDate") + .HasColumnType("datetime2"); + + b.Property("PlannedStartDate") + .HasColumnType("datetime2"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectPhase", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DeployStatus") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("HasAssignmentOverride") + .HasColumnType("bit"); + + b.Property("IsArchived") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OrderIndex") + .HasColumnType("int"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProjectPhases", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectSection", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SkillId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SkillId"); + + b.ToTable("ProjectSections"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectTask", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AllocatedTime") + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("HasAssignmentOverride") + .HasColumnType("bit"); + + b.Property("HasTimeOverride") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OrderIndex") + .HasColumnType("int"); + + b.Property("PhaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("PhaseId"); + + b.ToTable("ProjectTasks", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSection", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("CurrentAssignedUserId") + .HasColumnType("bigint"); + + b.Property("InitialDescription") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("InitialEstimatedHours") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("OriginalAssignedUserId") + .HasColumnType("bigint"); + + b.Property("SkillId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("TaskId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("SkillId"); + + b.HasIndex("TaskId"); + + b.ToTable("TaskSections", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSectionActivity", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EndNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("SectionId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("TaskSectionActivities", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSectionAdditionalTime", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AddedAt") + .HasColumnType("datetime2"); + + b.Property("AddedByUserId") + .HasColumnType("bigint"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Hours") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("Reason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("TaskSectionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("TaskSectionId"); + + b.ToTable("TaskSectionAdditionalTimes", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.RoleAgg.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("GozareshgirRoleId") + .HasColumnType("bigint"); + + b.Property("RoleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("PmRoles", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities.SalaryPaymentSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("EndSettingDate") + .HasColumnType("datetime2"); + + b.Property("HolidayWorking") + .HasColumnType("bit"); + + b.Property("MonthlySalary") + .HasColumnType("float"); + + b.Property("StartSettingDate") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("SalaryPaymentSetting", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.SkillAgg.Entities.Skill", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Skills", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DeletedDate") + .HasColumnType("datetime2"); + + b.Property("EditedDate") + .HasColumnType("datetime2"); + + b.Property("FileId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsEdited") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsPinned") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("PinnedByUserId") + .HasColumnType("bigint"); + + b.Property("PinnedDate") + .HasColumnType("datetime2"); + + b.Property("ReplyToMessageId") + .HasColumnType("uniqueidentifier"); + + b.Property("SenderUserId") + .HasColumnType("bigint"); + + b.Property("TaskId") + .HasColumnType("uniqueidentifier"); + + b.Property("TextContent") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.HasKey("Id"); + + b.HasIndex("CreationDate"); + + b.HasIndex("FileId"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("ReplyToMessageId"); + + b.HasIndex("SenderUserId"); + + b.HasIndex("TaskId"); + + b.HasIndex("TaskId", "IsPinned"); + + b.ToTable("TaskChatMessages", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ProfilePhotoPath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("VerifyCode") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.HasKey("Id"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.UserRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("UserRefreshTokens", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.PhaseSection", b => + { + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectPhase", "Phase") + .WithMany("PhaseSections") + .HasForeignKey("PhaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GozareshgirProgramManager.Domain.SkillAgg.Entities.Skill", "Skill") + .WithMany() + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Phase"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectPhase", b => + { + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project", "Project") + .WithMany("Phases") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectSection", b => + { + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project", "Project") + .WithMany("ProjectSections") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GozareshgirProgramManager.Domain.SkillAgg.Entities.Skill", "Skill") + .WithMany() + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Project"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectTask", b => + { + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectPhase", "Phase") + .WithMany("Tasks") + .HasForeignKey("PhaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Phase"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSection", b => + { + b.HasOne("GozareshgirProgramManager.Domain.SkillAgg.Entities.Skill", "Skill") + .WithMany("Sections") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectTask", "Task") + .WithMany("Sections") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Skill"); + + b.Navigation("Task"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSectionActivity", b => + { + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSection", "Section") + .WithMany("Activities") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSectionAdditionalTime", b => + { + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSection", null) + .WithMany("AdditionalTimes") + .HasForeignKey("TaskSectionId"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.RoleAgg.Entities.Role", b => + { + b.OwnsMany("GozareshgirProgramManager.Domain.PermissionAgg.Entities.Permission", "Permissions", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Code") + .HasColumnType("int"); + + b1.Property("RoleId") + .HasColumnType("bigint"); + + b1.HasKey("Id"); + + b1.HasIndex("RoleId"); + + b1.ToTable("PmRolePermissions", (string)null); + + b1.WithOwner("Role") + .HasForeignKey("RoleId"); + + b1.Navigation("Role"); + }); + + b.Navigation("Permissions"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities.SalaryPaymentSetting", b => + { + b.OwnsMany("GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities.WorkingHours", "WorkingHoursList", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("EndShiftOne") + .HasColumnType("time(0)"); + + b1.Property("EndShiftTwo") + .HasColumnType("time(0)"); + + b1.Property("HasRestTime") + .HasColumnType("bit"); + + b1.Property("HasShiftOne") + .HasColumnType("bit"); + + b1.Property("HasShiftTow") + .HasColumnType("bit"); + + b1.Property("IsActiveDay") + .HasColumnType("bit"); + + b1.Property("PersianDayOfWeek") + .HasColumnType("int"); + + b1.Property("RestTime") + .HasColumnType("time(0)"); + + b1.Property("SalaryPaymentSettingId") + .HasColumnType("bigint"); + + b1.Property("ShiftDurationInMinutes") + .HasColumnType("int"); + + b1.Property("StartShiftOne") + .HasColumnType("time(0)"); + + b1.Property("StartShiftTwo") + .HasColumnType("time(0)"); + + b1.HasKey("Id"); + + b1.HasIndex("SalaryPaymentSettingId"); + + b1.ToTable("WorkingHours", (string)null); + + b1.WithOwner("SalaryPaymentSetting") + .HasForeignKey("SalaryPaymentSettingId"); + + b1.Navigation("SalaryPaymentSetting"); + }); + + b.Navigation("WorkingHoursList"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", b => + { + b.HasOne("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", "ReplyToMessage") + .WithMany() + .HasForeignKey("ReplyToMessageId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ReplyToMessage"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.User", b => + { + b.OwnsMany("GozareshgirProgramManager.Domain.RoleUserAgg.RoleUser", "RoleUser", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("RoleId") + .HasColumnType("bigint"); + + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.HasKey("Id"); + + b1.HasIndex("UserId"); + + b1.ToTable("RoleUsers", (string)null); + + b1.WithOwner("User") + .HasForeignKey("UserId"); + + b1.Navigation("User"); + }); + + b.Navigation("RoleUser"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.UserRefreshToken", b => + { + b.HasOne("GozareshgirProgramManager.Domain.UserAgg.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project", b => + { + b.Navigation("Phases"); + + b.Navigation("ProjectSections"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectPhase", b => + { + b.Navigation("PhaseSections"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectTask", b => + { + b.Navigation("Sections"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSection", b => + { + b.Navigation("Activities"); + + b.Navigation("AdditionalTimes"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.SkillAgg.Entities.Skill", b => + { + b.Navigation("Sections"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.User", b => + { + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/20260105112925_add task chat - uploaded file.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/20260105112925_add task chat - uploaded file.cs new file mode 100644 index 00000000..65c9ed07 --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/20260105112925_add task chat - uploaded file.cs @@ -0,0 +1,158 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GozareshgirProgramManager.Infrastructure.Migrations +{ + /// + public partial class addtaskchatuploadedfile : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TaskChatMessages", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TaskId = table.Column(type: "uniqueidentifier", nullable: false), + SenderUserId = table.Column(type: "bigint", nullable: false), + MessageType = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + TextContent = table.Column(type: "nvarchar(4000)", maxLength: 4000, nullable: true), + FileId = table.Column(type: "uniqueidentifier", nullable: true), + ReplyToMessageId = table.Column(type: "uniqueidentifier", nullable: true), + IsEdited = table.Column(type: "bit", nullable: false, defaultValue: false), + EditedDate = table.Column(type: "datetime2", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeletedDate = table.Column(type: "datetime2", nullable: true), + IsPinned = table.Column(type: "bit", nullable: false, defaultValue: false), + PinnedDate = table.Column(type: "datetime2", nullable: true), + PinnedByUserId = table.Column(type: "bigint", nullable: true), + CreationDate = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TaskChatMessages", x => x.Id); + table.ForeignKey( + name: "FK_TaskChatMessages_TaskChatMessages_ReplyToMessageId", + column: x => x.ReplyToMessageId, + principalTable: "TaskChatMessages", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "UploadedFiles", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + OriginalFileName = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + UniqueFileName = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + FileExtension = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + FileSizeBytes = table.Column(type: "bigint", nullable: false), + MimeType = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + FileType = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Category = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + StorageProvider = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + StoragePath = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + StorageUrl = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + ThumbnailUrl = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + UploadedByUserId = table.Column(type: "bigint", nullable: false), + UploadDate = table.Column(type: "datetime2", nullable: false), + Status = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + ImageWidth = table.Column(type: "int", nullable: true), + ImageHeight = table.Column(type: "int", nullable: true), + DurationSeconds = table.Column(type: "int", nullable: true), + VirusScanDate = table.Column(type: "datetime2", nullable: true), + IsVirusScanPassed = table.Column(type: "bit", nullable: true), + VirusScanResult = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeletedDate = table.Column(type: "datetime2", nullable: true), + DeletedByUserId = table.Column(type: "bigint", nullable: true), + ReferenceEntityType = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + ReferenceEntityId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + CreationDate = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UploadedFiles", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_TaskChatMessages_CreationDate", + table: "TaskChatMessages", + column: "CreationDate"); + + migrationBuilder.CreateIndex( + name: "IX_TaskChatMessages_FileId", + table: "TaskChatMessages", + column: "FileId"); + + migrationBuilder.CreateIndex( + name: "IX_TaskChatMessages_IsDeleted", + table: "TaskChatMessages", + column: "IsDeleted"); + + migrationBuilder.CreateIndex( + name: "IX_TaskChatMessages_ReplyToMessageId", + table: "TaskChatMessages", + column: "ReplyToMessageId"); + + migrationBuilder.CreateIndex( + name: "IX_TaskChatMessages_SenderUserId", + table: "TaskChatMessages", + column: "SenderUserId"); + + migrationBuilder.CreateIndex( + name: "IX_TaskChatMessages_TaskId", + table: "TaskChatMessages", + column: "TaskId"); + + migrationBuilder.CreateIndex( + name: "IX_TaskChatMessages_TaskId_IsPinned", + table: "TaskChatMessages", + columns: new[] { "TaskId", "IsPinned" }); + + migrationBuilder.CreateIndex( + name: "IX_UploadedFiles_Category", + table: "UploadedFiles", + column: "Category"); + + migrationBuilder.CreateIndex( + name: "IX_UploadedFiles_IsDeleted", + table: "UploadedFiles", + column: "IsDeleted"); + + migrationBuilder.CreateIndex( + name: "IX_UploadedFiles_ReferenceEntityType_ReferenceEntityId", + table: "UploadedFiles", + columns: new[] { "ReferenceEntityType", "ReferenceEntityId" }); + + migrationBuilder.CreateIndex( + name: "IX_UploadedFiles_Status", + table: "UploadedFiles", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_UploadedFiles_UniqueFileName", + table: "UploadedFiles", + column: "UniqueFileName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UploadedFiles_UploadedByUserId", + table: "UploadedFiles", + column: "UploadedByUserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TaskChatMessages"); + + migrationBuilder.DropTable( + name: "UploadedFiles"); + } + } +} diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/AppDbContextModelSnapshot.cs index 4382f61b..2ac44b92 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/AppDbContextModelSnapshot.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -102,6 +102,131 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations b.ToTable("Customers", (string)null); }); + modelBuilder.Entity("GozareshgirProgramManager.Domain.FileManagementAgg.Entities.UploadedFile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DeletedByUserId") + .HasColumnType("bigint"); + + b.Property("DeletedDate") + .HasColumnType("datetime2"); + + b.Property("DurationSeconds") + .HasColumnType("int"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ImageHeight") + .HasColumnType("int"); + + b.Property("ImageWidth") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsVirusScanPassed") + .HasColumnType("bit"); + + b.Property("MimeType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ReferenceEntityId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ReferenceEntityType") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("StoragePath") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("StorageProvider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("StorageUrl") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ThumbnailUrl") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("UniqueFileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UploadDate") + .HasColumnType("datetime2"); + + b.Property("UploadedByUserId") + .HasColumnType("bigint"); + + b.Property("VirusScanDate") + .HasColumnType("datetime2"); + + b.Property("VirusScanResult") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("Status"); + + b.HasIndex("UniqueFileName") + .IsUnique(); + + b.HasIndex("UploadedByUserId"); + + b.HasIndex("ReferenceEntityType", "ReferenceEntityId"); + + b.ToTable("UploadedFiles", (string)null); + }); + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.PhaseSection", b => { b.Property("Id") @@ -495,6 +620,81 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations b.ToTable("Skills", (string)null); }); + modelBuilder.Entity("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DeletedDate") + .HasColumnType("datetime2"); + + b.Property("EditedDate") + .HasColumnType("datetime2"); + + b.Property("FileId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsEdited") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsPinned") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("PinnedByUserId") + .HasColumnType("bigint"); + + b.Property("PinnedDate") + .HasColumnType("datetime2"); + + b.Property("ReplyToMessageId") + .HasColumnType("uniqueidentifier"); + + b.Property("SenderUserId") + .HasColumnType("bigint"); + + b.Property("TaskId") + .HasColumnType("uniqueidentifier"); + + b.Property("TextContent") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.HasKey("Id"); + + b.HasIndex("CreationDate"); + + b.HasIndex("FileId"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("ReplyToMessageId"); + + b.HasIndex("SenderUserId"); + + b.HasIndex("TaskId"); + + b.HasIndex("TaskId", "IsPinned"); + + b.ToTable("TaskChatMessages", (string)null); + }); + modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.User", b => { b.Property("Id") @@ -779,6 +979,16 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations b.Navigation("WorkingHoursList"); }); + modelBuilder.Entity("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", b => + { + b.HasOne("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", "ReplyToMessage") + .WithMany() + .HasForeignKey("ReplyToMessageId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ReplyToMessage"); + }); + modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.User", b => { b.OwnsMany("GozareshgirProgramManager.Domain.RoleUserAgg.RoleUser", "RoleUser", b1 => diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Context/ProgramManagerDbContext.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Context/ProgramManagerDbContext.cs index 2fc4854c..01979e6f 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Context/ProgramManagerDbContext.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Context/ProgramManagerDbContext.cs @@ -1,14 +1,16 @@ -using GozareshgirProgramManager.Application._Common.Interfaces; +using GozareshgirProgramManager.Application._Common.Interfaces; using GozareshgirProgramManager.Domain.CheckoutAgg.Entities; using GozareshgirProgramManager.Domain.CustomerAgg; using GozareshgirProgramManager.Application._Common.Interfaces; using GozareshgirProgramManager.Domain.CustomerAgg; +using GozareshgirProgramManager.Domain.FileManagementAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.RoleAgg.Entities; using GozareshgirProgramManager.Domain.RoleUserAgg; using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities; using GozareshgirProgramManager.Domain.UserAgg.Entities; using GozareshgirProgramManager.Domain.SkillAgg.Entities; +using GozareshgirProgramManager.Domain.TaskChatAgg.Entities; using Microsoft.EntityFrameworkCore; namespace GozareshgirProgramManager.Infrastructure.Persistence.Context; @@ -40,6 +42,13 @@ public class ProgramManagerDbContext : DbContext, IProgramManagerDbContext public DbSet Roles { get; set; } = null!; public DbSet Skills { get; set; } = null!; + + // File Management + public DbSet UploadedFiles { get; set; } = null!; + + // Task Chat + public DbSet TaskChatMessages { get; set; } = null!; + protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(ProgramManagerDbContext).Assembly); diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Mappings/TaskChatMessageMapping.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Mappings/TaskChatMessageMapping.cs new file mode 100644 index 00000000..5675bb2d --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Mappings/TaskChatMessageMapping.cs @@ -0,0 +1,87 @@ +using GozareshgirProgramManager.Domain.TaskChatAgg.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace GozareshgirProgramManager.Infrastructure.Persistence.Mappings; + +public class TaskChatMessageMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("TaskChatMessages"); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .ValueGeneratedNever(); + + // Task Reference + builder.Property(x => x.TaskId) + .IsRequired(); + + builder.HasIndex(x => x.TaskId); + + // Sender + builder.Property(x => x.SenderUserId) + .IsRequired(); + + builder.HasIndex(x => x.SenderUserId); + + // Message Type + builder.Property(x => x.MessageType) + .IsRequired() + .HasConversion() + .HasMaxLength(50); + + // Content + builder.Property(x => x.TextContent) + .HasMaxLength(4000); + + // File Reference + builder.Property(x => x.FileId); + + builder.HasIndex(x => x.FileId); + + // Reply + builder.Property(x => x.ReplyToMessageId); + + builder.HasOne(x => x.ReplyToMessage) + .WithMany() + .HasForeignKey(x => x.ReplyToMessageId) + .OnDelete(DeleteBehavior.NoAction); + + // Status + builder.Property(x => x.IsEdited) + .IsRequired() + .HasDefaultValue(false); + + builder.Property(x => x.EditedDate); + + builder.Property(x => x.IsDeleted) + .IsRequired() + .HasDefaultValue(false); + + builder.Property(x => x.DeletedDate); + + builder.HasIndex(x => x.IsDeleted); + + // Pin + builder.Property(x => x.IsPinned) + .IsRequired() + .HasDefaultValue(false); + + builder.Property(x => x.PinnedDate); + builder.Property(x => x.PinnedByUserId); + + builder.HasIndex(x => new { x.TaskId, x.IsPinned }); + + // Audit + builder.Property(x => x.CreationDate) + .IsRequired(); + + builder.HasIndex(x => x.CreationDate); + + // Query Filter - پیام‌های حذف نشده + builder.HasQueryFilter(x => !x.IsDeleted); + } +} diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Mappings/UploadedFileMapping.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Mappings/UploadedFileMapping.cs new file mode 100644 index 00000000..f3ed3539 --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Mappings/UploadedFileMapping.cs @@ -0,0 +1,121 @@ +using GozareshgirProgramManager.Domain.FileManagementAgg.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace GozareshgirProgramManager.Infrastructure.Persistence.Mappings; + +public class UploadedFileMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("UploadedFiles"); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .ValueGeneratedNever(); + + // اطلاعات فایل + builder.Property(x => x.OriginalFileName) + .IsRequired() + .HasMaxLength(500); + + builder.Property(x => x.UniqueFileName) + .IsRequired() + .HasMaxLength(500); + + builder.HasIndex(x => x.UniqueFileName) + .IsUnique(); + + builder.Property(x => x.FileExtension) + .IsRequired() + .HasMaxLength(50); + + builder.Property(x => x.FileSizeBytes) + .IsRequired(); + + builder.Property(x => x.MimeType) + .IsRequired() + .HasMaxLength(200); + + builder.Property(x => x.FileType) + .IsRequired() + .HasConversion() + .HasMaxLength(50); + + builder.Property(x => x.Category) + .IsRequired() + .HasConversion() + .HasMaxLength(100); + + // ذخیره‌سازی + builder.Property(x => x.StorageProvider) + .IsRequired() + .HasConversion() + .HasMaxLength(50); + + builder.Property(x => x.StoragePath) + .HasMaxLength(1000); + + builder.Property(x => x.StorageUrl) + .HasMaxLength(1000); + + builder.Property(x => x.ThumbnailUrl) + .HasMaxLength(1000); + + // متادیتا + builder.Property(x => x.UploadedByUserId) + .IsRequired(); + + builder.Property(x => x.UploadDate) + .IsRequired(); + + builder.Property(x => x.Status) + .IsRequired() + .HasConversion() + .HasMaxLength(50); + + builder.HasIndex(x => x.Status); + builder.HasIndex(x => x.UploadedByUserId); + builder.HasIndex(x => x.Category); + + // اطلاعات تصویر + builder.Property(x => x.ImageWidth); + builder.Property(x => x.ImageHeight); + + // اطلاعات صوت/ویدیو + builder.Property(x => x.DurationSeconds); + + // امنیت + builder.Property(x => x.VirusScanDate); + builder.Property(x => x.IsVirusScanPassed); + builder.Property(x => x.VirusScanResult) + .HasMaxLength(500); + + // Soft Delete + builder.Property(x => x.IsDeleted) + .IsRequired() + .HasDefaultValue(false); + + builder.Property(x => x.DeletedDate); + builder.Property(x => x.DeletedByUserId); + + builder.HasIndex(x => x.IsDeleted); + + // Reference Tracking + builder.Property(x => x.ReferenceEntityType) + .HasMaxLength(100); + + builder.Property(x => x.ReferenceEntityId) + .HasMaxLength(100); + + builder.HasIndex(x => new { x.ReferenceEntityType, x.ReferenceEntityId }); + + // Audit + builder.Property(x => x.CreationDate) + .IsRequired(); + + // Query Filter - فایل‌های حذف نشده + builder.HasQueryFilter(x => !x.IsDeleted); + } +} diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/FileManagement/UploadedFileRepository.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/FileManagement/UploadedFileRepository.cs new file mode 100644 index 00000000..495ae321 --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/FileManagement/UploadedFileRepository.cs @@ -0,0 +1,134 @@ +using GozareshgirProgramManager.Domain.FileManagementAgg.Entities; +using GozareshgirProgramManager.Domain.FileManagementAgg.Enums; +using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories; +using GozareshgirProgramManager.Infrastructure.Persistence.Context; +using Microsoft.EntityFrameworkCore; + +namespace GozareshgirProgramManager.Infrastructure.Persistence.Repositories.FileManagement; + +public class UploadedFileRepository : IUploadedFileRepository +{ + private readonly ProgramManagerDbContext _context; + + public UploadedFileRepository(ProgramManagerDbContext context) + { + _context = context; + } + + public async Task GetByIdAsync(Guid fileId) + { + return await _context.UploadedFiles + .FirstOrDefaultAsync(x => x.Id == fileId); + } + + public async Task GetByUniqueFileNameAsync(string uniqueFileName) + { + return await _context.UploadedFiles + .FirstOrDefaultAsync(x => x.UniqueFileName == uniqueFileName); + } + + public async Task> GetUserFilesAsync(long userId, int pageNumber, int pageSize) + { + return await _context.UploadedFiles + .Where(x => x.UploadedByUserId == userId) + .OrderByDescending(x => x.UploadDate) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } + + public async Task> GetByCategoryAsync(FileCategory category, int pageNumber, int pageSize) + { + return await _context.UploadedFiles + .Where(x => x.Category == category) + .OrderByDescending(x => x.UploadDate) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } + + public async Task> GetByStatusAsync(FileStatus status, int pageNumber, int pageSize) + { + return await _context.UploadedFiles + .Where(x => x.Status == status) + .OrderByDescending(x => x.UploadDate) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } + + public async Task> GetByReferenceAsync(string entityType, string entityId) + { + return await _context.UploadedFiles + .Where(x => x.ReferenceEntityType == entityType && x.ReferenceEntityId == entityId) + .OrderByDescending(x => x.UploadDate) + .ToListAsync(); + } + + public async Task> SearchByNameAsync(string searchTerm, int pageNumber, int pageSize) + { + return await _context.UploadedFiles + .Where(x => x.OriginalFileName.Contains(searchTerm)) + .OrderByDescending(x => x.UploadDate) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } + + public async Task GetUserFilesCountAsync(long userId) + { + return await _context.UploadedFiles + .CountAsync(x => x.UploadedByUserId == userId); + } + + public async Task GetUserTotalFileSizeAsync(long userId) + { + return await _context.UploadedFiles + .Where(x => x.UploadedByUserId == userId) + .SumAsync(x => x.FileSizeBytes); + } + + public async Task> GetExpiredFilesAsync(DateTime olderThan) + { + return await _context.UploadedFiles + .IgnoreQueryFilters() // Include deleted files + .Where(x => x.IsDeleted && x.DeletedDate < olderThan) + .ToListAsync(); + } + + public async Task AddAsync(UploadedFile file) + { + await _context.UploadedFiles.AddAsync(file); + return file; + } + + public Task UpdateAsync(UploadedFile file) + { + _context.UploadedFiles.Update(file); + return Task.CompletedTask; + } + + public Task DeleteAsync(UploadedFile file) + { + _context.UploadedFiles.Remove(file); + return Task.CompletedTask; + } + + public async Task SaveChangesAsync() + { + return await _context.SaveChangesAsync(); + } + + public async Task ExistsAsync(Guid fileId) + { + return await _context.UploadedFiles + .AnyAsync(x => x.Id == fileId); + } + + public async Task ExistsByUniqueFileNameAsync(string uniqueFileName) + { + return await _context.UploadedFiles + .AnyAsync(x => x.UniqueFileName == uniqueFileName); + } +} + diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/ProjectPhaseRepository.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/ProjectPhaseRepository.cs index 81306c82..f00d4512 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/ProjectPhaseRepository.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/ProjectPhaseRepository.cs @@ -19,6 +19,7 @@ public class ProjectPhaseRepository : RepositoryBase, IProje public Task GetWithTasksAsync(Guid phaseId) { return _context.ProjectPhases + .Include(x=>x.PhaseSections) .Include(p => p.Tasks) .ThenInclude(t => t.Sections) .ThenInclude(s => s.Skill) diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/ProjectTaskRepository.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/ProjectTaskRepository.cs index 4e09e8cb..cbaa79fe 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/ProjectTaskRepository.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/ProjectTaskRepository.cs @@ -58,7 +58,7 @@ public class ProjectTaskRepository : RepositoryBase, IProject .ToListAsync(); } - public Task> GetByPriorityAsync(TaskPriority priority) + public Task> GetByPriorityAsync(ProjectTaskPriority priority) { return _context.ProjectTasks .Where(t => t.Priority == priority) diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskChat/TaskChatMessageRepository.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskChat/TaskChatMessageRepository.cs new file mode 100644 index 00000000..f6b0e87d --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskChat/TaskChatMessageRepository.cs @@ -0,0 +1,122 @@ +using GozareshgirProgramManager.Domain.TaskChatAgg.Entities; +using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories; +using GozareshgirProgramManager.Infrastructure.Persistence.Context; +using Microsoft.EntityFrameworkCore; + +namespace GozareshgirProgramManager.Infrastructure.Persistence.Repositories.TaskChat; + +public class TaskChatMessageRepository : ITaskChatMessageRepository +{ + private readonly ProgramManagerDbContext _context; + + public TaskChatMessageRepository(ProgramManagerDbContext context) + { + _context = context; + } + + public async Task GetByIdAsync(Guid messageId) + { + return await _context.TaskChatMessages + .Include(x => x.ReplyToMessage) + .FirstOrDefaultAsync(x => x.Id == messageId); + } + + public async Task> GetTaskMessagesAsync(Guid taskId, int pageNumber, int pageSize) + { + return await _context.TaskChatMessages + .Where(x => x.TaskId == taskId) + .Include(x => x.ReplyToMessage) + .OrderBy(x => x.CreationDate) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } + + public async Task GetTaskMessageCountAsync(Guid taskId) + { + return await _context.TaskChatMessages + .CountAsync(x => x.TaskId == taskId); + } + + public async Task> GetPinnedMessagesAsync(Guid taskId) + { + return await _context.TaskChatMessages + .Where(x => x.TaskId == taskId && x.IsPinned) + .Include(x => x.ReplyToMessage) + .OrderByDescending(x => x.PinnedDate) + .ToListAsync(); + } + + public async Task GetLastMessageAsync(Guid taskId) + { + return await _context.TaskChatMessages + .Where(x => x.TaskId == taskId) + .OrderByDescending(x => x.CreationDate) + .FirstOrDefaultAsync(); + } + + public async Task> SearchMessagesAsync(Guid taskId, string searchText, int pageNumber, int pageSize) + { + return await _context.TaskChatMessages + .Where(x => x.TaskId == taskId && + x.TextContent != null && + x.TextContent.Contains(searchText)) + .Include(x => x.ReplyToMessage) + .OrderByDescending(x => x.CreationDate) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } + + public async Task> GetUserMessagesAsync(Guid taskId, long userId, int pageNumber, int pageSize) + { + return await _context.TaskChatMessages + .Where(x => x.TaskId == taskId && x.SenderUserId == userId) + .Include(x => x.ReplyToMessage) + .OrderByDescending(x => x.CreationDate) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } + + public async Task> GetMediaMessagesAsync(Guid taskId, int pageNumber, int pageSize) + { + return await _context.TaskChatMessages + .Where(x => x.TaskId == taskId && x.FileId != null) + .Include(x => x.ReplyToMessage) + .OrderByDescending(x => x.CreationDate) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } + + public async Task AddAsync(TaskChatMessage message) + { + await _context.TaskChatMessages.AddAsync(message); + return message; + } + + public Task UpdateAsync(TaskChatMessage message) + { + _context.TaskChatMessages.Update(message); + return Task.CompletedTask; + } + + public Task DeleteAsync(TaskChatMessage message) + { + _context.TaskChatMessages.Remove(message); + return Task.CompletedTask; + } + + public async Task SaveChangesAsync() + { + return await _context.SaveChangesAsync(); + } + + public async Task ExistsAsync(Guid messageId) + { + return await _context.TaskChatMessages + .AnyAsync(x => x.Id == messageId); + } +} + diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs index 89fb05fd..ce590dc7 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs @@ -19,6 +19,7 @@ public class TaskSectionRepository:RepositoryBase,ITaskSection { return await _context.TaskSections .Include(x => x.Activities) + .Include(x=>x.AdditionalTimes) .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); } @@ -45,4 +46,20 @@ public class TaskSectionRepository:RepositoryBase,ITaskSection .Include(x => x.AdditionalTimes) .ToListAsync(cancellationToken); } + + public async Task HasUserAnyInProgressSectionAsync(long userId, CancellationToken cancellationToken = default) + { + return await _context.TaskSections + .AnyAsync(x => x.CurrentAssignedUserId == userId && x.Status == TaskSectionStatus.InProgress, + cancellationToken); + } + + public Task> GetAllNotCompletedOrPendingIncludeAllAsync(CancellationToken cancellationToken) + { + return _context.TaskSections + .Where(x => x.Status != TaskSectionStatus.Completed && x.Status != TaskSectionStatus.PendingForCompletion) + .Include(x => x.Activities) + .Include(x => x.AdditionalTimes) + .ToListAsync(cancellationToken); + } } \ No newline at end of file diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/LocalFileStorageService.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/LocalFileStorageService.cs new file mode 100644 index 00000000..9e534bbc --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/LocalFileStorageService.cs @@ -0,0 +1,108 @@ +using GozareshgirProgramManager.Application.Services.FileManagement; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + + +namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement; + +/// +/// سرویس ذخیره‌سازی فایل در سیستم فایل محلی +/// +public class LocalFileStorageService : IFileStorageService +{ + private readonly string _uploadBasePath; + private readonly string _baseUrl; + + public LocalFileStorageService(IConfiguration configuration, + IHttpContextAccessor httpContextAccessor, IHostEnvironment env) + { + // محاسبه مسیر پایه: اگر env نبود، از مسیر فعلی استفاده کن + var contentRoot = env.ContentRootPath; + _uploadBasePath = Path.Combine(contentRoot, "Storage"); + // Base URL برای دسترسی به فایل‌ها + var request = httpContextAccessor.HttpContext?.Request; + if (request != null) + { + _baseUrl = $"{request.Scheme}://{request.Host}/storage"; + } + else + { + _baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/storage"; + } + + // ایجاد پوشه اگر وجود نداشت + if (!Directory.Exists(_uploadBasePath)) + { + Directory.CreateDirectory(_uploadBasePath); + } + } + + public async Task<(string StoragePath, string StorageUrl)> UploadAsync( + Stream fileStream, + string uniqueFileName, + string category) + { + // ایجاد پوشه دسته‌بندی (مثلاً: Uploads/TaskChatMessage) + var categoryPath = Path.Combine(_uploadBasePath, category); + if (!Directory.Exists(categoryPath)) + { + Directory.CreateDirectory(categoryPath); + } + + // ایجاد زیرپوشه بر اساس تاریخ (مثلاً: Uploads/TaskChatMessage/2026/01) + var datePath = Path.Combine(categoryPath, DateTime.Now.Year.ToString(), + DateTime.Now.Month.ToString("00")); + if (!Directory.Exists(datePath)) + { + Directory.CreateDirectory(datePath); + } + + // مسیر کامل فایل + var storagePath = Path.Combine(datePath, uniqueFileName); + + // ذخیره فایل + await using var fileStreamOutput = new FileStream(storagePath, FileMode.Create, FileAccess.Write); + await fileStream.CopyToAsync(fileStreamOutput); + + // URL فایل + var relativePath = Path.GetRelativePath(_uploadBasePath, storagePath) + .Replace("\\", "/"); + var storageUrl = $"{_baseUrl}/{relativePath}"; + + return (storagePath, storageUrl); + } + + public Task DeleteAsync(string storagePath) + { + if (File.Exists(storagePath)) + { + File.Delete(storagePath); + } + + return Task.CompletedTask; + } + + public Task GetFileStreamAsync(string storagePath) + { + if (!File.Exists(storagePath)) + { + return Task.FromResult(null); + } + + var stream = new FileStream(storagePath, FileMode.Open, FileAccess.Read, FileShare.Read); + return Task.FromResult(stream); + } + + public Task ExistsAsync(string storagePath) + { + return Task.FromResult(File.Exists(storagePath)); + } + + public string GetFileUrl(string storagePath) + { + var relativePath = Path.GetRelativePath(_uploadBasePath, storagePath) + .Replace("\\", "/"); + return $"{_baseUrl}/{relativePath}"; + } +} diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/ThumbnailGeneratorService.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/ThumbnailGeneratorService.cs new file mode 100644 index 00000000..baf6366b --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/ThumbnailGeneratorService.cs @@ -0,0 +1,144 @@ +using GozareshgirProgramManager.Application.Services.FileManagement; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Formats.Jpeg; + +namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement; + +/// +/// سرویس تولید thumbnail با استفاده از ImageSharp +/// +public class ThumbnailGeneratorService : IThumbnailGeneratorService +{ + private readonly string _thumbnailBasePath; + private readonly string _baseUrl; + + public ThumbnailGeneratorService(IConfiguration configuration, + IHttpContextAccessor httpContextAccessor) + { + _thumbnailBasePath = configuration["FileStorage:ThumbnailPath"] + ?? Path.Combine(Directory.GetCurrentDirectory(), "storage", "Thumbnails"); + var request = httpContextAccessor.HttpContext?.Request; + + if (request != null) + { + _baseUrl = $"{request.Scheme}://{request.Host}/storage"; + } + else + { + _baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/storage"; + } + + if (!Directory.Exists(_thumbnailBasePath)) + { + Directory.CreateDirectory(_thumbnailBasePath); + } + } + + public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync( + string imagePath, + string category, + int width = 200, + int height = 200) + { + try + { + if (!File.Exists(imagePath)) + { + return null; + } + + // بارگذاری تصویر + using var image = await Image.LoadAsync(imagePath); + + // Resize با حفظ نسبت + image.Mutate(x => x.Resize(new ResizeOptions + { + Size = new Size(width, height), + Mode = ResizeMode.Max + })); + + // نام فایل thumbnail + var fileName = Path.GetFileNameWithoutExtension(imagePath); + var extension = Path.GetExtension(imagePath); + var thumbnailFileName = $"{fileName}_thumb{extension}"; + + // دریافت مسیر و URL توسط متد private + var (thumbnailPath, thumbnailUrl) = GetThumbnailPathAndUrl(thumbnailFileName, category); + + // ذخیره thumbnail با کیفیت 80 + await image.SaveAsync(thumbnailPath, new JpegEncoder { Quality = 80 }); + + + return (thumbnailPath, thumbnailUrl); + } + catch (Exception) + { + // در صورت خطا null برمی‌گردانیم + return null; + } + } + + public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(string videoPath, string category = "general") + { + // TODO: برای Video thumbnail باید از FFmpeg استفاده کنیم + // فعلاً یک placeholder image برمی‌گردانیم + await Task.CompletedTask; + return null; + } + + public Task DeleteThumbnailAsync(string thumbnailPath) + { + if (File.Exists(thumbnailPath)) + { + File.Delete(thumbnailPath); + } + + return Task.CompletedTask; + } + + public async Task<(int Width, int Height)?> GetImageDimensionsAsync(string imagePath) + { + try + { + if (!File.Exists(imagePath)) + { + return null; + } + + var imageInfo = await Image.IdentifyAsync(imagePath); + return (imageInfo.Width, imageInfo.Height); + } + catch (Exception) + { + return null; + } + } + + /// + /// دریافت مسیر فیزیکی و URL برای thumbnail بر اساس category + /// + private (string ThumbnailPath, string ThumbnailUrl) GetThumbnailPathAndUrl(string thumbnailFileName, string category) + { + var categoryFolder = string.IsNullOrWhiteSpace(category) ? "general" : category; + var categoryPath = Path.Combine(Directory.GetCurrentDirectory(), "storage", categoryFolder); + + if (!Directory.Exists(categoryPath)) + { + Directory.CreateDirectory(categoryPath); + } + + var thumbnailSubPath = Path.Combine(categoryPath, "Thumbnails"); + if (!Directory.Exists(thumbnailSubPath)) + { + Directory.CreateDirectory(thumbnailSubPath); + } + + var thumbnailPath = Path.Combine(thumbnailSubPath, thumbnailFileName); + var thumbnailUrl = $"{_baseUrl}/{categoryFolder}/thumbnails/{thumbnailFileName}"; + + return (thumbnailPath, thumbnailUrl); + } +} diff --git a/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs b/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs index e13bcc97..7be0b240 100644 --- a/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs +++ b/ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using GozareshgirProgramManager.Application._Common.Models; +using GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion; using GozareshgirProgramManager.Application.Modules.Projects.Commands.AssignProject; using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoStopOverTimeTaskSections; using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoUpdateDeployStatus; @@ -17,9 +18,12 @@ using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectBoar using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardDetail; using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardList; using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails; +using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch; using MediatR; using Microsoft.AspNetCore.Mvc; using ServiceHost.BaseControllers; +using GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeTaskPriority; +using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoPendingFullTimeTaskSections; namespace ServiceHost.Areas.Admin.Controllers.ProgramManager; @@ -40,6 +44,15 @@ public class ProjectController : ProgramManagerBaseController return res; } + [HttpGet("search")] + public async Task>> Search( + [FromQuery] string search) + { + var searchQuery = new GetProjectSearchQuery(search); + var res = await _mediator.Send(searchQuery); + return res; + } + [HttpPost] public async Task> Create([FromBody] CreateProjectCommand command) { @@ -111,6 +124,8 @@ public class ProjectController : ProgramManagerBaseController { // اجرای Command برای متوقف کردن تسک‌های overtime قبل از نمایش await _mediator.Send(new AutoStopOverTimeTaskSectionsCommand()); + // سپس تسک‌هایی که به 100% زمان تخمینی رسیده‌اند را به حالت PendingForCompletion ببریم + await _mediator.Send(new AutoPendingFullTimeTaskSectionsCommand()); var res = await _mediator.Send(query); return res; @@ -147,4 +162,19 @@ public class ProjectController : ProgramManagerBaseController var res = await _mediator.Send(command); return res; } + + [HttpPost("approve-completion")] + public async Task> ApproveTaskSectionCompletion([FromBody] ApproveTaskSectionCompletionCommand command) + { + var res = await _mediator.Send(command); + return res; + } + + [HttpPost("change-priority")] + public async Task> ChangePriority([FromBody] ChangeTaskPriorityCommand command) + { + var res = await _mediator.Send(command); + return res; + } + } \ No newline at end of file diff --git a/ServiceHost/Areas/Admin/Controllers/ProgramManager/TaskChatController.cs b/ServiceHost/Areas/Admin/Controllers/ProgramManager/TaskChatController.cs new file mode 100644 index 00000000..39983e00 --- /dev/null +++ b/ServiceHost/Areas/Admin/Controllers/ProgramManager/TaskChatController.cs @@ -0,0 +1,144 @@ +using GozareshgirProgramManager.Application._Common.Models; +using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.DeleteMessage; +using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.EditMessage; +using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.PinMessage; +using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.SendMessage; +using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.UnpinMessage; +using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs; +using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetMessages; +using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetPinnedMessages; +using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.SearchMessages; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using ServiceHost.BaseControllers; + +namespace ServiceHost.Areas.Admin.Controllers.ProgramManager; + +/// +/// کنترلر مدیریت چت تسک +/// +public class TaskChatController : ProgramManagerBaseController +{ + private readonly IMediator _mediator; + + public TaskChatController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// دریافت لیست پیام‌های یک تسک + /// + /// شناسه تسک + /// صفحه (پیش‌فرض: 1) + /// تعداد در هر صفحه (پیش‌فرض: 50) + [HttpGet("{taskId:guid}/messages")] + public async Task>>> GetMessages( + Guid taskId, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 50) + { + var query = new GetMessagesQuery(taskId, page, pageSize); + var result = await _mediator.Send(query); + return result; + } + + /// + /// دریافت پیام‌های پین شده یک تسک + /// + /// شناسه تسک + [HttpGet("{taskId:guid}/messages/pinned")] + public async Task>>> GetPinnedMessages(Guid taskId) + { + var query = new GetPinnedMessagesQuery(taskId); + var result = await _mediator.Send(query); + return result; + } + + /// + /// جستجو در پیام‌های یک تسک + /// + /// شناسه تسک + /// متن جستجو + /// صفحه + /// تعداد در هر صفحه + [HttpGet("{taskId:guid}/messages/search")] + public async Task>>> SearchMessages( + Guid taskId, + [FromQuery] string search, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20) + { + var query = new SearchMessagesQuery(taskId, search, page, pageSize); + var result = await _mediator.Send(query); + return result; + } + + /// + /// ارسال پیام جدید (با یا بدون فایل) + /// + [HttpPost("messages")] + public async Task>> SendMessage( + [FromForm] SendMessageCommand command) + { + var result = await _mediator.Send(command); + return result; + } + + /// + /// ویرایش پیام (فقط متن) + /// + /// شناسه پیام + /// محتوای جدید + [HttpPut("messages/{messageId:guid}")] + public async Task> EditMessage( + Guid messageId, + [FromBody] EditMessageRequest request) + { + var command = new EditMessageCommand(messageId, request.NewTextContent); + var result = await _mediator.Send(command); + return result; + } + + /// + /// حذف پیام + /// + /// شناسه پیام + [HttpDelete("messages/{messageId:guid}")] + public async Task> DeleteMessage(Guid messageId) + { + var command = new DeleteMessageCommand(messageId); + var result = await _mediator.Send(command); + return result; + } + + /// + /// پین کردن پیام + /// + /// شناسه پیام + [HttpPost("messages/{messageId:guid}/pin")] + public async Task> PinMessage(Guid messageId) + { + var command = new PinMessageCommand(messageId); + var result = await _mediator.Send(command); + return result; + } + + /// + /// برداشتن پین پیام + /// + /// شناسه پیام + [HttpPost("messages/{messageId:guid}/unpin")] + public async Task> UnpinMessage(Guid messageId) + { + var command = new UnpinMessageCommand(messageId); + var result = await _mediator.Send(command); + return result; + } +} + +public class EditMessageRequest +{ + public string NewTextContent { get; set; } = string.Empty; +} + diff --git a/ServiceHost/Areas/Admin/Controllers/institutionContractController.cs b/ServiceHost/Areas/Admin/Controllers/institutionContractController.cs index cb0e2408..17283a00 100644 --- a/ServiceHost/Areas/Admin/Controllers/institutionContractController.cs +++ b/ServiceHost/Areas/Admin/Controllers/institutionContractController.cs @@ -916,6 +916,17 @@ public class institutionContractController : AdminBaseController "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $"قرارداد های مالی.xlsx"); } + + /// + /// تنظیم وضعیت ارسال قرارداد + /// + [HttpPost("set-is-sent")] + public async Task> SetIsSent([FromBody] SetInstitutionContractSendFlagRequest request) + { + var result = await _institutionContractApplication.SetContractSendFlag(request); + return result; + } + } public class InstitutionContractCreationGetRepresentativeIdResponse @@ -969,4 +980,4 @@ public class VerifyCodeRequest { public long ContractingPartyId { get; set; } public string verifyCode { get; set; } -} \ No newline at end of file +} diff --git a/ServiceHost/Areas/AdminNew/Pages/Company/Ticket/Index.cshtml.cs b/ServiceHost/Areas/AdminNew/Pages/Company/Ticket/Index.cshtml.cs index bf33a3ed..11b18fd1 100644 --- a/ServiceHost/Areas/AdminNew/Pages/Company/Ticket/Index.cshtml.cs +++ b/ServiceHost/Areas/AdminNew/Pages/Company/Ticket/Index.cshtml.cs @@ -80,7 +80,7 @@ namespace ServiceHost.Areas.AdminNew.Pages.Company.Ticket public IActionResult OnGetShowDetailTicketByAdmin(long ticketID) { var res = _ticketApplication.GetDetails(ticketID); - res.WorkshopName = _workshopApplication.GetDetails(res.WorkshopId).WorkshopFullName; + res.WorkshopName = _workshopApplication.GetDetails(res.WorkshopId)?.WorkshopFullName??""; return Partial("DetailTicketModal", res); } diff --git a/ServiceHost/Program.cs b/ServiceHost/Program.cs index 921656cd..218871e3 100644 --- a/ServiceHost/Program.cs +++ b/ServiceHost/Program.cs @@ -72,6 +72,7 @@ Log.Logger = new LoggerConfiguration() //NO Microsoft Public log .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + //.MinimumLevel.Information() .WriteTo.File( path: Path.Combine(logDirectory, "gozareshgir_log.txt"), @@ -379,7 +380,31 @@ builder.Services.AddParbad().ConfigureGateways(gateways => }); -builder.Host.UseSerilog(); +if (builder.Environment.IsDevelopment()) +{ + builder.Host.UseSerilog((context, services, configuration) => + { + var logConfig = configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext(); + + + logConfig.WriteTo.File( + path: Path.Combine(logDirectory, "gozareshgir_log.txt"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 30, + shared: true, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}" + ); + }, writeToProviders: true); // این باعث میشه کنسول پیش‌فرض هم کار کنه + +} +else +{ + builder.Host.UseSerilog(); +} + Log.Information("SERILOG STARTED SUCCESSFULLY"); var app = builder.Build(); @@ -475,6 +500,24 @@ app.UseHttpsRedirection(); app.UseStaticFiles(); +// Static files برای فایل‌های آپلود شده +var uploadsPath = builder.Configuration["FileStorage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "Storage"); +if (!Directory.Exists(uploadsPath)) +{ + Directory.CreateDirectory(uploadsPath); +} + +app.UseStaticFiles(new StaticFileOptions +{ + FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(uploadsPath), + RequestPath = "/storage", + OnPrepareResponse = ctx => + { + // Cache برای فایل‌ها (30 روز) + ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000"); + } +}); + app.UseCookiePolicy(); diff --git a/ServiceHost/ServiceHost.csproj b/ServiceHost/ServiceHost.csproj index 8eb8a743..a6eb425b 100644 --- a/ServiceHost/ServiceHost.csproj +++ b/ServiceHost/ServiceHost.csproj @@ -10,6 +10,7 @@ true + a6049acf-0286-4947-983a-761d06d65f36