Merge branch 'master' into Feature/loan/client-api
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Company.Domain.InstitutionContractSendFlagAgg;
|
||||
|
||||
/// <summary>
|
||||
/// Interface برای Repository مربوط به فلگ ارسال قرارداد
|
||||
/// </summary>
|
||||
public interface IInstitutionContractSendFlagRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// ایجاد یک رکورد جدید برای فلگ ارسال قرارداد
|
||||
/// </summary>
|
||||
Task Create(InstitutionContractSendFlag flag);
|
||||
|
||||
/// <summary>
|
||||
/// بازیابی فلگ بر اساس شناسه قرارداد
|
||||
/// </summary>
|
||||
Task<InstitutionContractSendFlag> GetByContractId(long contractId);
|
||||
|
||||
/// <summary>
|
||||
/// بهروزرسانی فلگ ارسال
|
||||
/// </summary>
|
||||
Task Update(InstitutionContractSendFlag flag);
|
||||
|
||||
/// <summary>
|
||||
/// بررسی اینکه آیا قرارداد ارسال شده است
|
||||
/// </summary>
|
||||
Task<bool> IsContractSent(long contractId);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace Company.Domain.InstitutionContractSendFlagAgg;
|
||||
|
||||
/// <summary>
|
||||
/// نمایندگی فلگ ارسال قرارداد در MongoDB
|
||||
/// این موجودیت برای ردیابی اینکه آیا قرارداد ارسال شده است استفاده میشود
|
||||
/// </summary>
|
||||
public class InstitutionContractSendFlag
|
||||
{
|
||||
public InstitutionContractSendFlag(long institutionContractId,bool isSent)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
InstitutionContractId = institutionContractId;
|
||||
IsSent = isSent;
|
||||
CreatedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// شناسه یکتای MongoDB
|
||||
/// </summary>
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه قرارداد در SQL
|
||||
/// </summary>
|
||||
public long InstitutionContractId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// آیا قرارداد ارسال شده است
|
||||
/// </summary>
|
||||
public bool IsSent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// تاریخ و زمان ارسال
|
||||
/// </summary>
|
||||
public DateTime? SentDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// تاریخ و زمان ایجاد رکورد
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// تاریخ و زمان آخرین بهروزرسانی
|
||||
/// </summary>
|
||||
public DateTime? LastModifiedDate { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// علامتگذاری قرارداد به عنوان ارسالشده
|
||||
/// </summary>
|
||||
public void MarkAsSent()
|
||||
{
|
||||
IsSent = true;
|
||||
SentDate = DateTime.Now;
|
||||
LastModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بازگردانی علامت ارسال
|
||||
/// </summary>
|
||||
public void MarkAsNotSent()
|
||||
{
|
||||
IsSent = false;
|
||||
SentDate = null;
|
||||
LastModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بهروزرسانی علامت آخری اصلاح
|
||||
/// </summary>
|
||||
public void UpdateLastModified()
|
||||
{
|
||||
LastModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Company.Domain.InstitutionContractSendFlagAgg;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace CompanyManagement.Infrastructure.Mongo.InstitutionContractSendFlagRepo;
|
||||
|
||||
/// <summary>
|
||||
/// Repository برای مدیریت فلگ ارسال قرارداد در MongoDB
|
||||
/// </summary>
|
||||
public class InstitutionContractSendFlagRepository : IInstitutionContractSendFlagRepository
|
||||
{
|
||||
private readonly IMongoCollection<InstitutionContractSendFlag> _collection;
|
||||
|
||||
public InstitutionContractSendFlagRepository(IMongoDatabase database)
|
||||
{
|
||||
_collection = database.GetCollection<InstitutionContractSendFlag>("InstitutionContractSendFlag");
|
||||
}
|
||||
|
||||
public async Task Create(InstitutionContractSendFlag flag)
|
||||
{
|
||||
await _collection.InsertOneAsync(flag);
|
||||
}
|
||||
|
||||
public async Task<InstitutionContractSendFlag> GetByContractId(long contractId)
|
||||
{
|
||||
var filter = Builders<InstitutionContractSendFlag>.Filter
|
||||
.Eq(x => x.InstitutionContractId, contractId);
|
||||
|
||||
return await _collection.Find(filter).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task Update(InstitutionContractSendFlag flag)
|
||||
{
|
||||
var filter = Builders<InstitutionContractSendFlag>.Filter
|
||||
.Eq(x => x.InstitutionContractId, flag.InstitutionContractId);
|
||||
|
||||
await _collection.ReplaceOneAsync(filter, flag);
|
||||
}
|
||||
|
||||
public async Task<bool> IsContractSent(long contractId)
|
||||
{
|
||||
var flag = await GetByContractId(contractId);
|
||||
return flag != null && flag.IsSent;
|
||||
}
|
||||
|
||||
public async Task Remove(long contractId)
|
||||
{
|
||||
var filter = Builders<InstitutionContractSendFlag>.Filter
|
||||
.Eq(x => x.InstitutionContractId, contractId);
|
||||
|
||||
await _collection.DeleteOneAsync(filter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -96,6 +96,8 @@ public class GetInstitutionContractListItemsViewModel
|
||||
/// مبلغ قسط
|
||||
/// </summary>
|
||||
public double InstallmentAmount { get; set; }
|
||||
|
||||
public bool InstitutionContractIsSentFlag { get; set; }
|
||||
}
|
||||
|
||||
public class InstitutionContractListWorkshop
|
||||
|
||||
@@ -148,7 +148,7 @@ public interface IInstitutionContractApplication
|
||||
/// <param name="id">شناسه قرارداد</param>
|
||||
/// <returns>نتیجه عملیات</returns>
|
||||
OperationResult UnSign(long id);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// ایجاد حساب کاربری برای طرف قرارداد
|
||||
/// </summary>
|
||||
@@ -305,6 +305,14 @@ public interface IInstitutionContractApplication
|
||||
Task<InstitutionContractDiscountResponse> SetDiscountForCreation(InstitutionContractSetDiscountForCreationRequest request);
|
||||
Task<InstitutionContractDiscountResponse> ResetDiscountForCreation(InstitutionContractResetDiscountForExtensionRequest request);
|
||||
Task<OperationResult> CreationComplete(InstitutionContractExtensionCompleteRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// تعیین فلگ ارسال قرارداد در MongoDB
|
||||
/// اگر فلگ وجود نداشتند ایجاد میکند
|
||||
/// </summary>
|
||||
/// <param name="request">درخواست تعیین فلگ</param>
|
||||
/// <returns>نتیجه عملیات</returns>
|
||||
Task<OperationResult> SetContractSendFlag(SetInstitutionContractSendFlagRequest request);
|
||||
}
|
||||
|
||||
public class CreationSetContractingPartyResponse
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace CompanyManagment.App.Contracts.InstitutionContract;
|
||||
|
||||
/// <summary>
|
||||
/// درخواست برای تعیین فلگ ارسال قرارداد
|
||||
/// </summary>
|
||||
public class SetInstitutionContractSendFlagRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه قرارداد
|
||||
/// </summary>
|
||||
public long InstitutionContractId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// آیا قرارداد ارسال شده است
|
||||
/// </summary>
|
||||
public bool IsSent { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -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<SepehrPaymentGateway> sepehrGatewayLogger)
|
||||
ISepehrPaymentGatewayService sepehrPaymentGatewayService,ILogger<SepehrPaymentGateway> 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// تعیین فلگ ارسال قرارداد
|
||||
/// اگر فلگ وجود نداشتند ایجاد میکند
|
||||
/// </summary>
|
||||
public async Task<OperationResult> SetContractSendFlag(SetInstitutionContractSendFlagRequest request)
|
||||
{
|
||||
var operationResult = new OperationResult();
|
||||
|
||||
try
|
||||
{
|
||||
// بازیابی قرارداد از SQL
|
||||
var contract = _institutionContractRepository.Get(request.InstitutionContractId);
|
||||
if (contract == null)
|
||||
return operationResult.Failed("قرارداد مورد نظر یافت نشد");
|
||||
|
||||
// بررسی اینکه آیا فلگ در MongoDB وجود دارد
|
||||
var existingFlag = await _institutionContractSendFlagRepository
|
||||
.GetByContractId(request.InstitutionContractId);
|
||||
|
||||
if (existingFlag != null)
|
||||
{
|
||||
// اگر فلگ وجود داشتند، آن را اپدیت کنیم
|
||||
if (request.IsSent)
|
||||
{
|
||||
existingFlag.MarkAsSent();
|
||||
}
|
||||
else
|
||||
{
|
||||
existingFlag.MarkAsNotSent();
|
||||
}
|
||||
existingFlag.UpdateLastModified();
|
||||
await _institutionContractSendFlagRepository.Update(existingFlag);
|
||||
}
|
||||
else
|
||||
{
|
||||
// اگر فلگ وجود ندارد، آن را ایجاد کنیم
|
||||
var newFlag = new InstitutionContractSendFlag(
|
||||
request.InstitutionContractId,
|
||||
request.IsSent
|
||||
);
|
||||
|
||||
await _institutionContractSendFlagRepository.Create(newFlag);
|
||||
}
|
||||
|
||||
return operationResult.Succcedded();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return operationResult.Failed($"خطا در تعیین فلگ ارسال: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region CustomViewModels
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<long, InstitutionCon
|
||||
private readonly IMongoCollection<InstitutionContractExtensionTemp> _institutionExtensionTemp;
|
||||
private readonly IMongoCollection<InstitutionContractAmendmentTemp> _institutionAmendmentTemp;
|
||||
private readonly IMongoCollection<InstitutionContractCreationTemp> _institutionContractCreationTemp;
|
||||
private readonly IMongoCollection<InstitutionContractSendFlag> _institutionContractSendFlag;
|
||||
private readonly IPlanPercentageRepository _planPercentageRepository;
|
||||
private readonly ISmsService _smsService;
|
||||
private readonly ISmsResultRepository _smsResultRepository;
|
||||
@@ -114,6 +117,8 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
|
||||
database.GetCollection<InstitutionContractAmendmentTemp>("InstitutionContractAmendmentTemp");
|
||||
_institutionContractCreationTemp =
|
||||
database.GetCollection<InstitutionContractCreationTemp>("InstitutionContractCreationTemp");
|
||||
_institutionContractSendFlag =
|
||||
database.GetCollection<InstitutionContractSendFlag>("InstitutionContractSendFlag");
|
||||
}
|
||||
|
||||
public EditInstitutionContract GetDetails(long id)
|
||||
@@ -1353,6 +1358,12 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
|
||||
.Where(x => contractIds.Contains(x.InstitutionContractId))
|
||||
.ToDictionaryAsync(x => x.InstitutionContractId, x => x);
|
||||
|
||||
// بارگذاری وضعیت ارسال قراردادها از MongoDB - کوئری مستقیم
|
||||
var filter = Builders<InstitutionContractSendFlag>.Filter
|
||||
.In(x => x.InstitutionContractId, contractIds);
|
||||
var sendFlagsList = await _institutionContractSendFlag.Find(filter).ToListAsync();
|
||||
var sendFlags = sendFlagsList.ToDictionary(x => x.InstitutionContractId, x => x.IsSent);
|
||||
|
||||
var financialStatements = _context.FinancialStatments.Include(x => x.FinancialTransactionList)
|
||||
.Where(x => contractingPartyIds.Contains(x.ContractingPartyId)).ToList();
|
||||
var res = new PagedResult<GetInstitutionContractListItemsViewModel>()
|
||||
@@ -1462,7 +1473,8 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
|
||||
Workshops = workshopDetails,
|
||||
IsInPersonContract = workshopGroup?.CurrentWorkshops
|
||||
.Any(y => y.Services.ContractInPerson) ?? true,
|
||||
IsOldContract = x.contract.SigningType == InstitutionContractSigningType.Legacy
|
||||
IsOldContract = x.contract.SigningType == InstitutionContractSigningType.Legacy,
|
||||
InstitutionContractIsSentFlag = sendFlags.ContainsKey(x.contract.id) ? sendFlags[x.contract.id] : false
|
||||
};
|
||||
}).ToList()
|
||||
};
|
||||
@@ -2002,7 +2014,7 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
|
||||
DiscountedAmount = discountAmount.ToMoney(),
|
||||
DiscountPercetage = request.DiscountPercentage,
|
||||
Installments = InstitutionMonthlyInstallmentCaculation((int)request.Duration,
|
||||
totalAmount, DateTime.Now.ToFarsi()),
|
||||
paymentAmount, DateTime.Now.ToFarsi()),
|
||||
OneMonthAmount = discountedOneMonthAmount.ToMoney(),
|
||||
Obligation = totalAmount.ToMoney()
|
||||
};
|
||||
@@ -6334,10 +6346,10 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///دریافت لیست پیامک قرادا های آبی بدهکار
|
||||
///دریافت لیست پیامک قراداد های آبی بدهکار
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
//public async Task<List<SmsListData>> GetBlueSmsListData()
|
||||
//public async Task<List<SmsListData>> GetWarningSmsListData()
|
||||
//{
|
||||
|
||||
// var institutionContracts = await _context.InstitutionContractSet.AsQueryable().Select(x => new InstitutionContractViewModel
|
||||
@@ -6358,6 +6370,8 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
|
||||
// }).Where(x => x.IsActiveString == "blue" &&
|
||||
// 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<long, InstitutionCon
|
||||
// .Where(x => 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<long, InstitutionCon
|
||||
// {
|
||||
// var contractingParty = GetDetails(item.ContractingPartyId);
|
||||
// bool hasLegalActionSentSms = legalActionSentSms.Any(x => x.InstitutionContractId == item.Id);
|
||||
// int year = Convert.ToInt32(item.ContractEndFa.Substring(0, 4));
|
||||
// int month = Convert.ToInt32(item.ContractEndFa.Substring(5, 2));
|
||||
// var endOfContractNextMonthStart = new PersianDateTime(year, month, 1).AddMonths(1);
|
||||
// var endOfContractNextMonthEnd = (($"{endOfContractNextMonthStart}").FindeEndOfMonth()).ToGeorgianDateTime();
|
||||
// var now = DateTime.Now
|
||||
|
||||
// if (!string.IsNullOrWhiteSpace(contractingParty.LName) && !hasLegalActionSentSms)
|
||||
// if (!string.IsNullOrWhiteSpace(contractingParty.LName) && !hasLegalActionSentSms && now.Date <= endOfContractNextMonthEnd.Date)
|
||||
// {
|
||||
// //Thread.Sleep(500);
|
||||
// var partyName = contractingParty.LName;
|
||||
@@ -6425,7 +6444,7 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
|
||||
|
||||
// var debtor = transactions.FinancialTransactionViewModels.Sum(x => 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<long, InstitutionCon
|
||||
|
||||
// foreach (var number in phoneNumbers)
|
||||
// {
|
||||
|
||||
|
||||
// var isLastAlarmSend = _context.SmsResults.Any(x => (
|
||||
// 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)
|
||||
// {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ICameraBugReportApplication, CameraBugReportApplication>();
|
||||
services.AddTransient<ICameraBugReportRepository, CameraBugReportRepository>(); // MongoDB Implementation
|
||||
|
||||
// InstitutionContractSendFlag - MongoDB
|
||||
services.AddTransient<IInstitutionContractSendFlagRepository, InstitutionContractSendFlagRepository>();
|
||||
|
||||
services.AddDbContext<CompanyContext>(x => x.UseSqlServer(connectionString));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||
<PackageReference Include="MediatR" Version="14.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
@@ -18,4 +19,10 @@
|
||||
<ProjectReference Include="..\..\Domain\GozareshgirProgramManager.Domain\GozareshgirProgramManager.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Features">
|
||||
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.AspNetCore.Http.Features.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||
using GozareshgirProgramManager.Application._Common.Models;
|
||||
using GozareshgirProgramManager.Domain._Common;
|
||||
using GozareshgirProgramManager.Domain._Common.Exceptions;
|
||||
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
|
||||
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
|
||||
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion;
|
||||
|
||||
public record ApproveTaskSectionCompletionCommand(Guid TaskSectionId, bool IsApproved) : IBaseCommand;
|
||||
|
||||
public class ApproveTaskSectionCompletionCommandHandler : IBaseCommandHandler<ApproveTaskSectionCompletionCommand>
|
||||
{
|
||||
private readonly ITaskSectionRepository _taskSectionRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IAuthHelper _authHelper;
|
||||
|
||||
public ApproveTaskSectionCompletionCommandHandler(
|
||||
ITaskSectionRepository taskSectionRepository,
|
||||
IUnitOfWork unitOfWork,
|
||||
IAuthHelper authHelper)
|
||||
{
|
||||
_taskSectionRepository = taskSectionRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_authHelper = authHelper;
|
||||
}
|
||||
|
||||
public async Task<OperationResult> Handle(ApproveTaskSectionCompletionCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var currentUserId = _authHelper.GetCurrentUserId()
|
||||
?? throw new UnAuthorizedException("˜ÇÑÈÑ ÇÍÑÇÒ åæ?Ê äÔÏå ÇÓÊ");
|
||||
|
||||
var section = await _taskSectionRepository.GetByIdAsync(request.TaskSectionId, cancellationToken);
|
||||
if (section == null)
|
||||
{
|
||||
return OperationResult.NotFound("ÈÎÔ ãæÑÏ äÙÑ ?ÇÝÊ äÔÏ");
|
||||
}
|
||||
|
||||
if (section.Status != TaskSectionStatus.PendingForCompletion)
|
||||
{
|
||||
return OperationResult.Failure("ÝÞØ ÈÎÔ<C38E>åÇ?? ˜å ÏÑ ÇäÊÙÇÑ Ê˜ã?á åÓÊäÏ ÞÇÈá ÊÇ??Ï ?Ç ÑÏ åÓÊäÏ");
|
||||
}
|
||||
|
||||
if (request.IsApproved)
|
||||
{
|
||||
section.UpdateStatus(TaskSectionStatus.Completed);
|
||||
}
|
||||
else
|
||||
{
|
||||
section.UpdateStatus(TaskSectionStatus.Incomplete);
|
||||
}
|
||||
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return OperationResult.Success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion;
|
||||
|
||||
public class ApproveTaskSectionCompletionCommandValidator : AbstractValidator<ApproveTaskSectionCompletionCommand>
|
||||
{
|
||||
public ApproveTaskSectionCompletionCommandValidator()
|
||||
{
|
||||
RuleFor(c => c.TaskSectionId)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.WithMessage("ÔäÇÓå ÈÎÔ äã?<3F>ÊæÇäÏ ÎÇá? ÈÇÔÏ");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||
using GozareshgirProgramManager.Application._Common.Models;
|
||||
using GozareshgirProgramManager.Domain._Common;
|
||||
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
|
||||
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
|
||||
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoPendingFullTimeTaskSections;
|
||||
|
||||
public record AutoPendingFullTimeTaskSectionsCommand : IBaseCommand;
|
||||
|
||||
public class AutoPendingFullTimeTaskSectionsCommandHandler : IBaseCommandHandler<AutoPendingFullTimeTaskSectionsCommand>
|
||||
{
|
||||
private readonly ITaskSectionRepository _taskSectionRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public AutoPendingFullTimeTaskSectionsCommandHandler(
|
||||
ITaskSectionRepository taskSectionRepository,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
_taskSectionRepository = taskSectionRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<OperationResult> Handle(AutoPendingFullTimeTaskSectionsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// تمام سکشنهایی که هنوز Pending یا Completed نشدهاند را دریافت کن
|
||||
var taskSections = await _taskSectionRepository.GetAllNotCompletedOrPendingIncludeAllAsync(cancellationToken);
|
||||
|
||||
foreach (var section in taskSections)
|
||||
{
|
||||
var totalSpent = section.GetTotalTimeSpent();
|
||||
var estimate = section.FinalEstimatedHours;
|
||||
|
||||
if (estimate.TotalMinutes <= 0)
|
||||
continue; // تسک بدون تخمین را نادیده بگیر
|
||||
|
||||
if (totalSpent >= estimate)
|
||||
{
|
||||
// مهم: وضعیت را مستقل از فعال/غیرفعال بودن فعالیتها PendingForCompletion کنیم
|
||||
if (section.IsInProgress())
|
||||
{
|
||||
// اگر فعالیت فعال دارد، با وضعیت جدید متوقف شود
|
||||
section.StopWork(TaskSectionStatus.PendingForCompletion, "اتمام خودکار - رسیدن به ۱۰۰٪ زمان تخمینی");
|
||||
}
|
||||
else
|
||||
{
|
||||
section.UpdateStatus(TaskSectionStatus.PendingForCompletion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
return OperationResult.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return OperationResult.Failure($"خطا در در انتظار تکمیل قرار دادن خودکار تسکها: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,10 @@ public class ChangeStatusSectionCommandHandler : IBaseCommandHandler<ChangeStatu
|
||||
// Going TO InProgress: Check if section has remaining time, then start work
|
||||
if (!section.HasRemainingTime())
|
||||
return OperationResult.ValidationError("زمان این بخش به پایان رسیده است");
|
||||
|
||||
if (await _taskSectionRepository.HasUserAnyInProgressSectionAsync(section.CurrentAssignedUserId, cancellationToken))
|
||||
{
|
||||
return OperationResult.ValidationError("کاربر مورد نظر در حال حاضر بخش دیگری را در وضعیت 'درحال انجام' دارد");
|
||||
}
|
||||
section.StartWork();
|
||||
}
|
||||
else
|
||||
@@ -86,9 +89,9 @@ public class ChangeStatusSectionCommandHandler : IBaseCommandHandler<ChangeStatu
|
||||
var validTransitions = new Dictionary<TaskSectionStatus, List<TaskSectionStatus>>
|
||||
{
|
||||
{ TaskSectionStatus.ReadyToStart, [TaskSectionStatus.InProgress] },
|
||||
{ TaskSectionStatus.InProgress, [TaskSectionStatus.Incomplete, TaskSectionStatus.Completed] },
|
||||
{ TaskSectionStatus.Incomplete, [TaskSectionStatus.InProgress, TaskSectionStatus.Completed] },
|
||||
{ TaskSectionStatus.Completed, [TaskSectionStatus.InProgress, TaskSectionStatus.Incomplete] }, // Can return to InProgress or Incomplete
|
||||
{ TaskSectionStatus.InProgress, [TaskSectionStatus.Incomplete, TaskSectionStatus.PendingForCompletion] },
|
||||
{ TaskSectionStatus.Incomplete, [TaskSectionStatus.InProgress, TaskSectionStatus.PendingForCompletion] },
|
||||
{ TaskSectionStatus.PendingForCompletion, [TaskSectionStatus.InProgress, TaskSectionStatus.Incomplete] }, // Can return to InProgress or Incomplete
|
||||
{ TaskSectionStatus.NotAssigned, [TaskSectionStatus.InProgress, TaskSectionStatus.ReadyToStart] }
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||
using GozareshgirProgramManager.Application._Common.Models;
|
||||
using GozareshgirProgramManager.Domain._Common;
|
||||
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
|
||||
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
|
||||
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeTaskPriority;
|
||||
|
||||
public record ChangeTaskPriorityCommand(
|
||||
Guid Id,
|
||||
ProjectHierarchyLevel Level,
|
||||
ProjectTaskPriority Priority
|
||||
) : IBaseCommand;
|
||||
|
||||
public class ChangeTaskPriorityCommandHandler : IBaseCommandHandler<ChangeTaskPriorityCommand>
|
||||
{
|
||||
private readonly IProjectTaskRepository _taskRepository;
|
||||
private readonly IProjectPhaseRepository _phaseRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public ChangeTaskPriorityCommandHandler(
|
||||
IProjectTaskRepository taskRepository,
|
||||
IProjectPhaseRepository phaseRepository,
|
||||
IProjectRepository projectRepository,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
_taskRepository = taskRepository;
|
||||
_phaseRepository = phaseRepository;
|
||||
_projectRepository = projectRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<OperationResult> Handle(ChangeTaskPriorityCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
switch (request.Level)
|
||||
{
|
||||
case ProjectHierarchyLevel.Task:
|
||||
return await HandleTaskLevelAsync(request.Id, request.Priority, cancellationToken);
|
||||
case ProjectHierarchyLevel.Phase:
|
||||
return await HandlePhaseLevelAsync(request.Id, request.Priority, cancellationToken);
|
||||
case ProjectHierarchyLevel.Project:
|
||||
return await HandleProjectLevelAsync(request.Id, request.Priority, cancellationToken);
|
||||
default:
|
||||
return OperationResult.Failure("سطح نامعتبر است");
|
||||
}
|
||||
}
|
||||
|
||||
// Task-level priority update
|
||||
private async Task<OperationResult> HandleTaskLevelAsync(Guid taskId, ProjectTaskPriority priority, CancellationToken ct)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(taskId, ct);
|
||||
if (task is null)
|
||||
return OperationResult.NotFound("تسک یافت نشد");
|
||||
|
||||
if (task.Priority != priority)
|
||||
{
|
||||
task.SetPriority(priority);
|
||||
}
|
||||
|
||||
await _unitOfWork.SaveChangesAsync(ct);
|
||||
return OperationResult.Success();
|
||||
}
|
||||
|
||||
// Phase-level bulk priority update
|
||||
private async Task<OperationResult> HandlePhaseLevelAsync(Guid phaseId, ProjectTaskPriority priority, CancellationToken ct)
|
||||
{
|
||||
var phase = await _phaseRepository.GetWithTasksAsync(phaseId);
|
||||
if (phase is null)
|
||||
return OperationResult.NotFound("فاز یافت نشد");
|
||||
|
||||
var tasks = phase.Tasks?.ToList() ?? new List<Domain.ProjectAgg.Entities.ProjectTask>();
|
||||
foreach (var t in tasks)
|
||||
{
|
||||
if (t.Priority != priority)
|
||||
{
|
||||
t.SetPriority(priority);
|
||||
}
|
||||
}
|
||||
|
||||
await _unitOfWork.SaveChangesAsync(ct);
|
||||
return OperationResult.Success();
|
||||
}
|
||||
|
||||
// Project-level bulk priority update across all phases
|
||||
private async Task<OperationResult> HandleProjectLevelAsync(Guid projectId, ProjectTaskPriority priority, CancellationToken ct)
|
||||
{
|
||||
var project = await _projectRepository.GetWithFullHierarchyAsync(projectId);
|
||||
if (project is null)
|
||||
return OperationResult.NotFound("پروژه یافت نشد");
|
||||
|
||||
var phases = project.Phases?.ToList() ?? new List<Domain.ProjectAgg.Entities.ProjectPhase>();
|
||||
foreach (var phase in phases)
|
||||
{
|
||||
var tasks = phase.Tasks?.ToList() ?? new List<Domain.ProjectAgg.Entities.ProjectTask>();
|
||||
foreach (var t in tasks)
|
||||
{
|
||||
if (t.Priority != priority)
|
||||
{
|
||||
t.SetPriority(priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _unitOfWork.SaveChangesAsync(ct);
|
||||
return OperationResult.Success();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,15 @@ using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
|
||||
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimeProject;
|
||||
|
||||
public record SetTimeProjectCommand(List<SetTimeProjectSectionItem> SectionItems, Guid Id, ProjectHierarchyLevel Level):IBaseCommand;
|
||||
public record SetTimeProjectCommand(
|
||||
List<SetTimeProjectSkillItem> 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; }
|
||||
}
|
||||
@@ -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<SetTimeProjectCo
|
||||
private readonly IProjectPhaseRepository _projectPhaseRepository;
|
||||
private readonly IProjectTaskRepository _projectTaskRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IAuthHelper _authHelper;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ISkillRepository _skillRepository;
|
||||
private readonly IPhaseSectionRepository _phaseSectionRepository;
|
||||
private readonly IProjectSectionRepository _projectSectionRepository;
|
||||
private long? _userId;
|
||||
private readonly ITaskSectionRepository _taskSectionRepository;
|
||||
|
||||
|
||||
public SetTimeProjectCommandHandler(
|
||||
IProjectRepository projectRepository,
|
||||
IProjectPhaseRepository projectPhaseRepository,
|
||||
IProjectTaskRepository projectTaskRepository,
|
||||
IUnitOfWork unitOfWork, IAuthHelper authHelper)
|
||||
IUnitOfWork unitOfWork, IAuthHelper authHelper,
|
||||
IUserRepository userRepository, ISkillRepository skillRepository,
|
||||
IPhaseSectionRepository phaseSectionRepository,
|
||||
IProjectSectionRepository projectSectionRepository,
|
||||
ITaskSectionRepository taskSectionRepository)
|
||||
{
|
||||
_projectRepository = projectRepository;
|
||||
_projectPhaseRepository = projectPhaseRepository;
|
||||
_projectTaskRepository = projectTaskRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_authHelper = authHelper;
|
||||
_userRepository = userRepository;
|
||||
_skillRepository = skillRepository;
|
||||
_phaseSectionRepository = phaseSectionRepository;
|
||||
_projectSectionRepository = projectSectionRepository;
|
||||
_taskSectionRepository = taskSectionRepository;
|
||||
_userId = authHelper.GetCurrentUserId();
|
||||
}
|
||||
|
||||
@@ -37,6 +51,10 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
|
||||
{
|
||||
switch (request.Level)
|
||||
{
|
||||
case ProjectHierarchyLevel.Project:
|
||||
return await AssignProject(request);
|
||||
case ProjectHierarchyLevel.Phase:
|
||||
return await AssignProjectPhase(request);
|
||||
case ProjectHierarchyLevel.Task:
|
||||
return await SetTimeForProjectTask(request, cancellationToken);
|
||||
default:
|
||||
@@ -44,67 +62,229 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<OperationResult> SetTimeForProject(SetTimeProjectCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
private async Task<OperationResult> AssignProject(SetTimeProjectCommand request)
|
||||
{
|
||||
var project = await _projectRepository.GetWithFullHierarchyAsync(request.Id);
|
||||
if (project == null)
|
||||
if (project is null)
|
||||
{
|
||||
return OperationResult.NotFound("پروژه یافت نشد");
|
||||
return OperationResult.NotFound("<22><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD>");
|
||||
}
|
||||
|
||||
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<OperationResult> SetTimeForProjectPhase(SetTimeProjectCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
private async Task<OperationResult> AssignProjectPhase(SetTimeProjectCommand request)
|
||||
{
|
||||
var phase = await _projectPhaseRepository.GetWithTasksAsync(request.Id);
|
||||
if (phase == null)
|
||||
if (phase is null)
|
||||
{
|
||||
return OperationResult.NotFound("فاز پروژه یافت نشد");
|
||||
return OperationResult.NotFound("<22><><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD>");
|
||||
}
|
||||
|
||||
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<OperationResult> SetTimeForProjectTask(SetTimeProjectCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -116,24 +296,64 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
|
||||
|
||||
long? addedByUserId = _userId;
|
||||
|
||||
// تنظیم زمان مستقیماً برای sections این تسک
|
||||
foreach (var section in task.Sections)
|
||||
var validSkills = request.SkillItems
|
||||
.Where(x=>x.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<SetTimeProjectCo
|
||||
|
||||
section.ClearAdditionalTimes();
|
||||
// افزودن زمانهای اضافی
|
||||
bool hasAdditionalTime = false;
|
||||
foreach (var additionalTime in sectionItem.AdditionalTime)
|
||||
{
|
||||
var additionalTimeSpan = TimeSpan.FromHours(additionalTime.Hours);
|
||||
var additionalTimeSpan = TimeSpan.FromHours(additionalTime.Hours).Add(TimeSpan.FromMinutes(additionalTime.Minutes));
|
||||
section.AddAdditionalTime(additionalTimeSpan, additionalTime.Description, addedByUserId);
|
||||
hasAdditionalTime = true;
|
||||
}
|
||||
|
||||
// تغییر status به Incomplete فقط اگر زمان اضافی اضافه شده باشد و در وضعیتی غیر از ReadyToStart باشد
|
||||
if (hasAdditionalTime && section.Status != TaskSectionStatus.ReadyToStart)
|
||||
{
|
||||
// اگر سکشن درحال انجام است، باید متوقف شود قبل از تغییر status
|
||||
if (section.Status == TaskSectionStatus.InProgress)
|
||||
{
|
||||
section.StopWork(TaskSectionStatus.Incomplete);
|
||||
}
|
||||
else
|
||||
{
|
||||
section.UpdateStatus(TaskSectionStatus.Incomplete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,19 +13,15 @@ public class SetTimeProjectCommandValidator:AbstractValidator<SetTimeProjectComm
|
||||
.NotNull()
|
||||
.WithMessage("شناسه پروژه نمیتواند خالی باشد.");
|
||||
|
||||
RuleForEach(x => 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<SetTimeProjectSectionItem>
|
||||
public class SetTimeProjectSkillItemValidator:AbstractValidator<SetTimeProjectSkillItem>
|
||||
{
|
||||
public SetTimeProjectSectionItemValidator()
|
||||
public SetTimeProjectSkillItemValidator()
|
||||
{
|
||||
RuleFor(x=>x.SectionId)
|
||||
RuleFor(x=>x.SkillId)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.WithMessage("شناسه بخش نمیتواند خالی باشد.");
|
||||
@@ -47,6 +43,18 @@ public class AdditionalTimeDataValidator: AbstractValidator<SetTimeSectionTime>
|
||||
.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 کاراکتر باشد.");
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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<SetTimeSectionTime> AdditionalTime { get; set; } = [];
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
|
||||
|
||||
/// <summary>
|
||||
/// درخواست جستجو در سراسر سلسلهمراتب پروژه (پروژه، فاز، تسک).
|
||||
/// نتایج با اطلاعات مسیر سلسلهمراتب برای پشتیبانی از ناوبری درخت در رابط کاربری بازگردانده میشود.
|
||||
/// </summary>
|
||||
public record GetProjectSearchQuery(
|
||||
string SearchQuery) : IBaseQuery<GetProjectSearchResponse>;
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
using GozareshgirProgramManager.Application._Common.Interfaces;
|
||||
using GozareshgirProgramManager.Application._Common.Models;
|
||||
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
|
||||
|
||||
/// <summary>
|
||||
/// Handler برای درخواست جستجوی سراسری در سلسلهمراتب پروژه.
|
||||
/// این handler در تمام سطحهای پروژه، فاز و تسک جستجو میکند و از تمام فیلدهای متنی (نام، توضیحات) استفاده میکند.
|
||||
/// همچنین در زیرمجموعههای هر سطح (ProjectSections، PhaseSections، TaskSections) جستجو میکند.
|
||||
/// </summary>
|
||||
public class GetProjectSearchQueryHandler : IBaseQueryHandler<GetProjectSearchQuery, GetProjectSearchResponse>
|
||||
{
|
||||
private readonly IProgramManagerDbContext _context;
|
||||
private const int MaxResults = 50;
|
||||
|
||||
public GetProjectSearchQueryHandler(IProgramManagerDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<OperationResult<GetProjectSearchResponse>> Handle(
|
||||
GetProjectSearchQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var searchQuery = request.SearchQuery.ToLower();
|
||||
var results = new List<ProjectHierarchySearchResultDto>();
|
||||
|
||||
// جستجو در پروژهها و ProjectSections
|
||||
var projects = await SearchProjects(searchQuery, cancellationToken);
|
||||
results.AddRange(projects);
|
||||
|
||||
// جستجو در فازها و PhaseSections
|
||||
var phases = await SearchPhases(searchQuery, cancellationToken);
|
||||
results.AddRange(phases);
|
||||
|
||||
// جستجو در تسکها و TaskSections
|
||||
var tasks = await SearchTasks(searchQuery, cancellationToken);
|
||||
results.AddRange(tasks);
|
||||
|
||||
// مرتبسازی نتایج: ابتدا بر اساس سطح سلسلهمراتب (پروژه → فاز → تسک)، سپس بر اساس نام
|
||||
var sortedResults = results
|
||||
.OrderBy(r => r.Level)
|
||||
.ThenBy(r => r.Title)
|
||||
.Take(MaxResults)
|
||||
.ToList();
|
||||
|
||||
var response = new GetProjectSearchResponse(sortedResults);
|
||||
return OperationResult<GetProjectSearchResponse>.Success(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// جستجو در جدول پروژهها (نام، توضیحات) و ProjectSections (نام مهارت، توضیحات اولیه)
|
||||
/// </summary>
|
||||
private async Task<List<ProjectHierarchySearchResultDto>> SearchProjects(
|
||||
string searchQuery,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var projects = await _context.Projects
|
||||
.Where(p =>
|
||||
p.Name.ToLower().Contains(searchQuery) ||
|
||||
(p.Description != null && p.Description.ToLower().Contains(searchQuery)))
|
||||
.Select(p => new ProjectHierarchySearchResultDto
|
||||
{
|
||||
Id = p.Id,
|
||||
Title = p.Name,
|
||||
Level = ProjectHierarchyLevel.Project,
|
||||
ProjectId = null,
|
||||
PhaseId = null
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// جستجو در جدول فازهای پروژه (نام، توضیحات) و PhaseSections
|
||||
/// </summary>
|
||||
private async Task<List<ProjectHierarchySearchResultDto>> SearchPhases(
|
||||
string searchQuery,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var phases = await _context.ProjectPhases
|
||||
.Where(ph =>
|
||||
ph.Name.ToLower().Contains(searchQuery) ||
|
||||
(ph.Description != null && ph.Description.ToLower().Contains(searchQuery)))
|
||||
.Select(ph => new ProjectHierarchySearchResultDto
|
||||
{
|
||||
Id = ph.Id,
|
||||
Title = ph.Name,
|
||||
Level = ProjectHierarchyLevel.Phase,
|
||||
ProjectId = ph.ProjectId,
|
||||
PhaseId = null
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return phases;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// جستجو در جدول تسکهای پروژه (نام، توضیحات) و TaskSections (نام مهارت، توضیح اولیه، اطلاعات اضافی)
|
||||
/// </summary>
|
||||
private async Task<List<ProjectHierarchySearchResultDto>> SearchTasks(
|
||||
string searchQuery,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tasks = await _context.ProjectTasks
|
||||
.Include(t => t.Sections)
|
||||
.Include(t => t.Phase)
|
||||
.Where(t =>
|
||||
t.Name.ToLower().Contains(searchQuery) ||
|
||||
(t.Description != null && t.Description.ToLower().Contains(searchQuery)) ||
|
||||
t.Sections.Any(s =>
|
||||
(s.InitialDescription != null && s.InitialDescription.ToLower().Contains(searchQuery)) ||
|
||||
s.AdditionalTimes.Any(at => at.Reason != null && at.Reason.ToLower().Contains(searchQuery))))
|
||||
.Select(t => new ProjectHierarchySearchResultDto
|
||||
{
|
||||
Id = t.Id,
|
||||
Title = t.Name,
|
||||
Level = ProjectHierarchyLevel.Task,
|
||||
ProjectId = t.Phase.ProjectId,
|
||||
PhaseId = t.PhaseId
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
|
||||
|
||||
/// <summary>
|
||||
/// اعتبارسنج برای درخواست جستجوی سراسری
|
||||
/// </summary>
|
||||
public class GetProjectSearchQueryValidator : AbstractValidator<GetProjectSearchQuery>
|
||||
{
|
||||
public GetProjectSearchQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.SearchQuery)
|
||||
.NotEmpty().WithMessage("متن جستجو نمیتواند خالی باشد.")
|
||||
.MinimumLength(2).WithMessage("متن جستجو باید حداقل 2 حرف باشد.")
|
||||
.MaximumLength(500).WithMessage("متن جستجو نمیتواند بیش از 500 حرف باشد.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
|
||||
|
||||
/// <summary>
|
||||
/// پوستهی پاسخ برای نتایج جستجوی سراسری
|
||||
/// </summary>
|
||||
public record GetProjectSearchResponse(
|
||||
List<ProjectHierarchySearchResultDto> Results);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
|
||||
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
|
||||
|
||||
/// <summary>
|
||||
/// DTO برای نتایج جستجوی سراسری در سلسلهمراتب پروژه.
|
||||
/// حاوی اطلاعات کافی برای بازسازی مسیر سلسلهمراتب و بسط درخت در رابط کاربری است.
|
||||
/// </summary>
|
||||
public record ProjectHierarchySearchResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه آیتم (پروژه، فاز یا تسک)
|
||||
/// </summary>
|
||||
public Guid Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// نام/عنوان آیتم
|
||||
/// </summary>
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// سطح سلسلهمراتب این آیتم
|
||||
/// </summary>
|
||||
public ProjectHierarchyLevel Level { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه پروژه - همیشه برای فاز و تسک پر شده است، برای پروژه با شناسه خود پر میشود
|
||||
/// </summary>
|
||||
public Guid? ProjectId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه فاز - فقط برای تسک پر شده است، برای پروژه و فاز خالی است
|
||||
/// </summary>
|
||||
public Guid? PhaseId { get; init; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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<GetProjectsListQuer
|
||||
|
||||
public async Task<OperationResult<GetProjectsListResponse>> Handle(GetProjectsListQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
List<GetProjectListDto> projects;
|
||||
var projects = new List<GetProjectDto>();
|
||||
var phases = new List<GetPhaseDto>();
|
||||
var tasks = new List<GetTaskDto>();
|
||||
|
||||
switch (request.HierarchyLevel)
|
||||
{
|
||||
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<GetProjectsListResponse>.Failure("سطح سلسله مراتب نامعتبر است");
|
||||
}
|
||||
await SetSkillFlags(projects, cancellationToken);
|
||||
|
||||
var response = new GetProjectsListResponse(projects);
|
||||
var response = new GetProjectsListResponse(projects, phases, tasks);
|
||||
return OperationResult<GetProjectsListResponse>.Success(response);
|
||||
}
|
||||
|
||||
private async Task<List<GetProjectListDto>> GetProjects(Guid? parentId, CancellationToken cancellationToken)
|
||||
private async Task<List<GetProjectDto>> GetProjects(Guid? parentId, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.Projects.AsQueryable();
|
||||
|
||||
// پروژهها سطح بالا هستند و parentId ندارند، فقط در صورت null بودن parentId نمایش داده میشوند
|
||||
if (parentId.HasValue)
|
||||
{
|
||||
return new List<GetProjectListDto>(); // پروژهها parent ندارند
|
||||
return new List<GetProjectDto>();
|
||||
}
|
||||
|
||||
var projects = await query
|
||||
var entities = await query
|
||||
.OrderByDescending(p => p.CreationDate)
|
||||
.ToListAsync(cancellationToken);
|
||||
var result = new List<GetProjectListDto>();
|
||||
|
||||
foreach (var project in projects)
|
||||
var result = new List<GetProjectDto>();
|
||||
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<List<GetProjectListDto>> GetPhases(Guid? parentId, CancellationToken cancellationToken)
|
||||
private async Task<List<GetPhaseDto>> 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<GetProjectListDto>();
|
||||
|
||||
foreach (var phase in phases)
|
||||
var result = new List<GetPhaseDto>();
|
||||
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<List<GetProjectListDto>> GetTasks(Guid? parentId, CancellationToken cancellationToken)
|
||||
private async Task<List<GetTaskDto>> 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<GetProjectListDto>();
|
||||
var result = new List<GetTaskDto>();
|
||||
// دریافت تمام Skills
|
||||
var allSkills = await _context.Skills
|
||||
.Select(s => new { s.Id, s.Name })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var task in tasks)
|
||||
foreach (var task in entities)
|
||||
{
|
||||
var percentage = 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<GetProjectListDto> projects, CancellationToken cancellationToken)
|
||||
private async Task SetSkillFlags<TItem>(List<TItem> 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<GetProjectListDto> projects, List<Guid> projectIds, CancellationToken cancellationToken)
|
||||
|
||||
private async Task SetSkillFlagsForProjects<TItem>(List<TItem> items, List<Guid> projectIds, CancellationToken cancellationToken) where TItem : GetProjectItemDto
|
||||
{
|
||||
var projectSections = await _context.ProjectSections
|
||||
.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<GetProjectListDto> projects, List<Guid> phaseIds, CancellationToken cancellationToken)
|
||||
private async Task SetSkillFlagsForPhases<TItem>(List<TItem> items, List<Guid> phaseIds, CancellationToken cancellationToken) where TItem : GetProjectItemDto
|
||||
{
|
||||
var phaseSections = await _context.PhaseSections
|
||||
.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<GetProjectListDto> projects, List<Guid> 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<int> 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<int>();
|
||||
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<int> 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<int>();
|
||||
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<int> 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<int>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
|
||||
|
||||
public record GetProjectsListResponse(
|
||||
List<GetProjectListDto> Projects);
|
||||
|
||||
List<GetProjectDto> Projects,
|
||||
List<GetPhaseDto> Phases,
|
||||
List<GetTaskDto> Tasks);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
|
||||
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
|
||||
|
||||
public class GetTaskDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public int Percentage { get; init; }
|
||||
public ProjectHierarchyLevel Level { get; init; }
|
||||
public Guid? ParentId { get; init; }
|
||||
public int TotalHours { get; set; }
|
||||
public int Minutes { get; set; }
|
||||
|
||||
// Task-specific fields
|
||||
public TimeSpan SpentTime { get; init; }
|
||||
public TimeSpan RemainingTime { get; init; }
|
||||
public ProjectTaskPriority Priority { get; set; }
|
||||
public List<GetTaskSectionDto> Sections { get; init; }
|
||||
}
|
||||
|
||||
public class GetTaskSectionDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string SkillName { get; init; } = string.Empty;
|
||||
public TimeSpan SpentTime { get; init; }
|
||||
public TimeSpan TotalTime { get; init; }
|
||||
public int Percentage { get; init; }
|
||||
public string UserFullName{ get; init; } = string.Empty;
|
||||
public AssignmentStatus AssignmentStatus { get; set; }
|
||||
|
||||
}
|
||||
@@ -6,5 +6,6 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.Project
|
||||
|
||||
public record ProjectBoardListQuery: IBaseQuery<List<ProjectBoardListResponse>>
|
||||
{
|
||||
public long? UserId { get; set; }
|
||||
public TaskSectionStatus? Status { get; set; }
|
||||
}
|
||||
@@ -40,6 +40,11 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
|
||||
{
|
||||
queryable = queryable.Where(x => x.Status == request.Status);
|
||||
}
|
||||
|
||||
if (request.UserId is > 0)
|
||||
{
|
||||
queryable = queryable.Where(x => x.CurrentAssignedUserId == request.UserId);
|
||||
}
|
||||
|
||||
var data = await queryable.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -53,68 +58,87 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
|
||||
.ToDictionaryAsync(x => x.Id, x => x.FullName, cancellationToken);
|
||||
|
||||
|
||||
var result = data.Select(x =>
|
||||
{
|
||||
// محاسبه یکبار برای هر Activity و Cache کردن نتیجه
|
||||
var activityTimeData = x.Activities.Select(a =>
|
||||
var result = data
|
||||
.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<ProjectProgressHistoryDto>();
|
||||
foreach (var activityData in activityTimeData)
|
||||
{
|
||||
var lastHistory = mergedHistories.LastOrDefault();
|
||||
|
||||
// اگر آخرین history برای همین کاربر باشد، زمانها را جمع میکنیم
|
||||
if (lastHistory != null && lastHistory.UserId == activityData.Activity.UserId)
|
||||
{
|
||||
var totalTimeSpan = lastHistory.WorkedTimeSpan + activityData.TimeSpent;
|
||||
lastHistory.WorkedTimeSpan = totalTimeSpan;
|
||||
lastHistory.WorkedTime = totalTimeSpan.ToString(@"hh\:mm");
|
||||
}
|
||||
else
|
||||
{
|
||||
// در غیر این صورت، یک history جدید اضافه میکنیم
|
||||
mergedHistories.Add(new ProjectProgressHistoryDto()
|
||||
{
|
||||
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<ProjectProgressHistoryDto>();
|
||||
foreach (var activityData in activityTimeData)
|
||||
{
|
||||
var lastHistory = mergedHistories.LastOrDefault();
|
||||
|
||||
// اگر آخرین history برای همین کاربر باشد، زمانها را جمع میکنیم
|
||||
if (lastHistory != null && lastHistory.UserId == activityData.Activity.UserId)
|
||||
{
|
||||
var totalTimeSpan = lastHistory.WorkedTimeSpan + activityData.TimeSpent;
|
||||
lastHistory.WorkedTimeSpan = totalTimeSpan;
|
||||
lastHistory.WorkedTime = totalTimeSpan.ToString(@"hh\:mm");
|
||||
}
|
||||
else
|
||||
{
|
||||
// در غیر این صورت، یک history جدید اضافه میکنیم
|
||||
mergedHistories.Add(new ProjectProgressHistoryDto()
|
||||
{
|
||||
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<List<ProjectBoardListResponse>>.Success(result);
|
||||
}
|
||||
|
||||
private static int GetStatusOrder(TaskSectionStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
TaskSectionStatus.InProgress => 0,
|
||||
TaskSectionStatus.Incomplete => 1,
|
||||
TaskSectionStatus.NotAssigned => 2,
|
||||
TaskSectionStatus.ReadyToStart => 2,
|
||||
TaskSectionStatus.PendingForCompletion => 3,
|
||||
_ => 99
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<ProjectProgressHistoryDto> Histories { get; set; }
|
||||
}
|
||||
|
||||
@@ -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<ProjectDeployBoardDetailItemSkill> 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);
|
||||
|
||||
|
||||
@@ -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<ProjectDeployBoardListItem> Items);
|
||||
@@ -66,7 +67,8 @@ public class ProjectDeployBoardListQueryHandler:IBaseQueryHandler<GetProjectDepl
|
||||
.Select(x => 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<GetProjectsDeployBoardListResponse>.Success(response);
|
||||
|
||||
@@ -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<ProjectSetTimeResponse>;
|
||||
public record ProjectSetTimeResponse(
|
||||
List<ProjectSetTimeResponseSections> SectionItems,
|
||||
List<ProjectSetTimeResponseSkill> 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<ProjectSetTimeResponseSectionAdditionalTime> 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; }
|
||||
}
|
||||
|
||||
@@ -22,17 +22,30 @@ public class ProjectSetTimeDetailsQueryHandler
|
||||
|
||||
public async Task<OperationResult<ProjectSetTimeResponse>> 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<ProjectSetTimeResponse>.Failure("سطح معادل نامعتبر است")
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<OperationResult<ProjectSetTimeResponse>> 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<ProjectSetTimeResponse>.NotFound("Project not found");
|
||||
return OperationResult<ProjectSetTimeResponse>.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<ProjectSetTimeResponse>.Success(res);
|
||||
}
|
||||
|
||||
private async Task<OperationResult<ProjectSetTimeResponse>> 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<ProjectSetTimeResponse>.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<ProjectSetTimeResponse>.Success(res);
|
||||
}
|
||||
|
||||
private async Task<OperationResult<ProjectSetTimeResponse>> 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<ProjectSetTimeResponse>.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<ProjectSetTimeResponse>.Success(res);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
using FluentValidation;
|
||||
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
|
||||
|
||||
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails;
|
||||
|
||||
public class ProjectSetTimeDetailsQueryValidator:AbstractValidator<ProjectSetTimeDetailsQuery>
|
||||
public class ProjectSetTimeDetailsQueryValidator : AbstractValidator<ProjectSetTimeDetailsQuery>
|
||||
{
|
||||
public ProjectSetTimeDetailsQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.TaskId)
|
||||
RuleFor(x => x.Id)
|
||||
.NotEmpty()
|
||||
.WithMessage("شناسه پروژه نمیتواند خالی باشد.");
|
||||
.WithMessage("شناسه نمیتواند خالی باشد.");
|
||||
|
||||
RuleFor(x => x.Level)
|
||||
.IsInEnum()
|
||||
.WithMessage("سطح معادل نامعتبر است.");
|
||||
}
|
||||
}
|
||||
@@ -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<DeleteMessageCommand>
|
||||
{
|
||||
private readonly ITaskChatMessageRepository _repository;
|
||||
private readonly IAuthHelper _authHelper;
|
||||
|
||||
public DeleteMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
|
||||
{
|
||||
_repository = repository;
|
||||
_authHelper = authHelper;
|
||||
}
|
||||
|
||||
public async Task<OperationResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<EditMessageCommand>
|
||||
{
|
||||
private readonly ITaskChatMessageRepository _repository;
|
||||
private readonly IAuthHelper _authHelper;
|
||||
|
||||
public EditMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
|
||||
{
|
||||
_repository = repository;
|
||||
_authHelper = authHelper;
|
||||
}
|
||||
|
||||
public async Task<OperationResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<PinMessageCommand>
|
||||
{
|
||||
private readonly ITaskChatMessageRepository _repository;
|
||||
private readonly IAuthHelper _authHelper;
|
||||
|
||||
public PinMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
|
||||
{
|
||||
_repository = repository;
|
||||
_authHelper = authHelper;
|
||||
}
|
||||
|
||||
public async Task<OperationResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<MessageDto>;
|
||||
|
||||
public class SendMessageCommandHandler : IBaseCommandHandler<SendMessageCommand, MessageDto>
|
||||
{
|
||||
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<OperationResult<MessageDto>> 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<MessageDto>.NotFound("تسک یافت نشد");
|
||||
}
|
||||
|
||||
Guid? uploadedFileId = null;
|
||||
if (request.File != null)
|
||||
{
|
||||
if (request.File.Length == 0)
|
||||
{
|
||||
return OperationResult<MessageDto>.ValidationError("فایل خالی است");
|
||||
}
|
||||
|
||||
const long maxFileSize = 100 * 1024 * 1024;
|
||||
if (request.File.Length > maxFileSize)
|
||||
{
|
||||
return OperationResult<MessageDto>.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<MessageDto>.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<MessageDto>.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;
|
||||
}
|
||||
}
|
||||
@@ -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<UnpinMessageCommand>
|
||||
{
|
||||
private readonly ITaskChatMessageRepository _repository;
|
||||
private readonly IAuthHelper _authHelper;
|
||||
|
||||
public UnpinMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
|
||||
{
|
||||
_repository = repository;
|
||||
_authHelper = authHelper;
|
||||
}
|
||||
|
||||
public async Task<OperationResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PaginationResult<MessageDto>>;
|
||||
|
||||
|
||||
|
||||
public class GetMessagesQueryHandler : IBaseQueryHandler<GetMessagesQuery, PaginationResult<MessageDto>>
|
||||
{
|
||||
private readonly IProgramManagerDbContext _context;
|
||||
private readonly IAuthHelper _authHelper;
|
||||
|
||||
public GetMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper)
|
||||
{
|
||||
_context = context;
|
||||
_authHelper = authHelper;
|
||||
}
|
||||
|
||||
private List<MessageDto> CreateAdditionalTimeNotes(
|
||||
IEnumerable<Domain.ProjectAgg.Entities.TaskSectionAdditionalTime> additionalTimes,
|
||||
Dictionary<long, string> users,
|
||||
Guid taskId)
|
||||
{
|
||||
var notes = new List<MessageDto>();
|
||||
|
||||
foreach (var additionalTime in additionalTimes)
|
||||
{
|
||||
var addedByUserName = additionalTime.AddedByUserId.HasValue && users.TryGetValue(additionalTime.AddedByUserId.Value, out var user)
|
||||
? user
|
||||
: "سیستم";
|
||||
|
||||
var noteContent = $"⏱️ زمان اضافی: {additionalTime.Hours.TotalHours.ToString("F2")} ساعت - {(string.IsNullOrWhiteSpace(additionalTime.Reason) ? "بدون علت" : additionalTime.Reason)} - توسط {addedByUserName}";
|
||||
|
||||
var noteDto = new MessageDto
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TaskId = taskId,
|
||||
SenderUserId = 0,
|
||||
SenderName = "سیستم",
|
||||
MessageType = "Note",
|
||||
TextContent = noteContent,
|
||||
CreationDate = additionalTime.CreationDate,
|
||||
IsMine = false
|
||||
};
|
||||
|
||||
notes.Add(noteDto);
|
||||
}
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
public async Task<OperationResult<PaginationResult<MessageDto>>> Handle(GetMessagesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
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<MessageDto>();
|
||||
|
||||
// ✅ ابتدا زمانهای اضافی قبل از اولین پیام را اضافه کن (اگر پیامی وجود داشته باشد)
|
||||
if (messages.Any())
|
||||
{
|
||||
var firstMessageDate = messages.First().CreationDate;
|
||||
var additionalTimesBeforeFirstMessage = allAdditionalTimes
|
||||
.Where(at => at.CreationDate < firstMessageDate)
|
||||
.ToList();
|
||||
|
||||
messageDtos.AddRange(CreateAdditionalTimeNotes(additionalTimesBeforeFirstMessage, users, request.TaskId));
|
||||
}
|
||||
else
|
||||
{
|
||||
// ✅ اگر هیچ پیامی وجود ندارد، همه زمانهای اضافی را نمایش بده
|
||||
messageDtos.AddRange(CreateAdditionalTimeNotes(allAdditionalTimes, users, request.TaskId));
|
||||
}
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
// ✅ نام فرستنده را از 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<MessageDto>()
|
||||
{
|
||||
List = messageDtos,
|
||||
TotalCount = totalCount,
|
||||
};
|
||||
|
||||
return OperationResult<PaginationResult<MessageDto>>.Success(response);
|
||||
}
|
||||
}
|
||||
@@ -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<List<MessageDto>>;
|
||||
|
||||
public class GetPinnedMessagesQueryHandler : IBaseQueryHandler<GetPinnedMessagesQuery, List<MessageDto>>
|
||||
{
|
||||
private readonly IProgramManagerDbContext _context;
|
||||
private readonly IAuthHelper _authHelper;
|
||||
|
||||
public GetPinnedMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper)
|
||||
{
|
||||
_context = context;
|
||||
_authHelper = authHelper;
|
||||
}
|
||||
|
||||
public async Task<OperationResult<List<MessageDto>>> 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<MessageDto>();
|
||||
|
||||
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<List<MessageDto>>.Success(messageDtos);
|
||||
}
|
||||
}
|
||||
@@ -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<List<MessageDto>>;
|
||||
|
||||
public class SearchMessagesQueryHandler : IBaseQueryHandler<SearchMessagesQuery, List<MessageDto>>
|
||||
{
|
||||
private readonly IProgramManagerDbContext _context;
|
||||
private readonly IAuthHelper _authHelper;
|
||||
|
||||
public SearchMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper)
|
||||
{
|
||||
_context = context;
|
||||
_authHelper = authHelper;
|
||||
}
|
||||
|
||||
public async Task<OperationResult<List<MessageDto>>> 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<MessageDto>();
|
||||
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<List<MessageDto>>.Success(messageDtos);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
|
||||
|
||||
namespace GozareshgirProgramManager.Application.Services.FileManagement;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس ذخیرهسازی فایل
|
||||
/// </summary>
|
||||
public interface IFileStorageService
|
||||
{
|
||||
/// <summary>
|
||||
/// آپلود فایل
|
||||
/// </summary>
|
||||
Task<(string StoragePath, string StorageUrl)> UploadAsync(
|
||||
Stream fileStream,
|
||||
string uniqueFileName,
|
||||
string category);
|
||||
|
||||
/// <summary>
|
||||
/// حذف فایل
|
||||
/// </summary>
|
||||
Task DeleteAsync(string storagePath);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت فایل
|
||||
/// </summary>
|
||||
Task<Stream?> GetFileStreamAsync(string storagePath);
|
||||
|
||||
/// <summary>
|
||||
/// بررسی وجود فایل
|
||||
/// </summary>
|
||||
Task<bool> ExistsAsync(string storagePath);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت URL فایل
|
||||
/// </summary>
|
||||
string GetFileUrl(string storagePath);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace GozareshgirProgramManager.Application.Services.FileManagement;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس تولید thumbnail برای تصاویر و ویدیوها
|
||||
/// </summary>
|
||||
public interface IThumbnailGeneratorService
|
||||
{
|
||||
/// <summary>
|
||||
/// تولید thumbnail برای تصویر
|
||||
/// </summary>
|
||||
Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync(
|
||||
string imagePath,
|
||||
string category,
|
||||
int width = 200,
|
||||
int height = 200);
|
||||
|
||||
/// <summary>
|
||||
/// تولید thumbnail برای ویدیو
|
||||
/// </summary>
|
||||
Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(
|
||||
string videoPath,
|
||||
string category);
|
||||
|
||||
/// <summary>
|
||||
/// حذف thumbnail
|
||||
/// </summary>
|
||||
Task DeleteThumbnailAsync(string thumbnailPath);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت ابعاد تصویر
|
||||
/// </summary>
|
||||
Task<(int Width, int Height)?> GetImageDimensionsAsync(string imagePath);
|
||||
}
|
||||
|
||||
@@ -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<ProjectTask> ProjectTasks { get; set; }
|
||||
|
||||
DbSet<TaskChatMessage> TaskChatMessages { get; set; }
|
||||
DbSet<UploadedFile> UploadedFiles { get; set; }
|
||||
|
||||
DbSet<Skill> Skills { get; set; }
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// فایل آپلود شده - Aggregate Root
|
||||
/// مدیریت مرکزی تمام فایلهای سیستم
|
||||
/// </summary>
|
||||
public class UploadedFile : EntityBase<Guid>
|
||||
{
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// دستهبندی فایل - مشخص میکند فایل در کجا استفاده شده
|
||||
/// </summary>
|
||||
public enum FileCategory
|
||||
{
|
||||
TaskChatMessage = 1, // پیام چت تسک
|
||||
TaskAttachment = 2, // ضمیمه تسک
|
||||
ProjectDocument = 3, // مستندات پروژه
|
||||
UserProfilePhoto = 4, // عکس پروفایل کاربر
|
||||
Report = 5, // گزارش
|
||||
Other = 6 // سایر
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// وضعیت فایل
|
||||
/// </summary>
|
||||
public enum FileStatus
|
||||
{
|
||||
Uploading = 1, // در حال آپلود
|
||||
Active = 2, // فعال و قابل استفاده
|
||||
Deleted = 5, // حذف شده (Soft Delete)
|
||||
Archived = 6 // آرشیو شده
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// نوع فایل
|
||||
/// </summary>
|
||||
public enum FileType
|
||||
{
|
||||
Document = 1, // اسناد (PDF, Word, Excel, etc.)
|
||||
Image = 2, // تصویر
|
||||
Video = 3, // ویدیو
|
||||
Audio = 4, // صوت
|
||||
Archive = 5, // فایل فشرده (ZIP, RAR)
|
||||
Other = 6 // سایر
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// نوع ذخیرهساز فایل
|
||||
/// </summary>
|
||||
public enum StorageProvider
|
||||
{
|
||||
LocalFileSystem = 1, // دیسک محلی سرور
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
|
||||
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
|
||||
|
||||
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository برای مدیریت فایلهای آپلود شده
|
||||
/// </summary>
|
||||
public interface IUploadedFileRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// دریافت فایل بر اساس شناسه
|
||||
/// </summary>
|
||||
Task<UploadedFile?> GetByIdAsync(Guid fileId);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت فایل بر اساس نام یکتا
|
||||
/// </summary>
|
||||
Task<UploadedFile?> GetByUniqueFileNameAsync(string uniqueFileName);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت لیست فایلهای یک کاربر
|
||||
/// </summary>
|
||||
Task<List<UploadedFile>> GetUserFilesAsync(long userId, int pageNumber, int pageSize);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت فایلهای یک دسته خاص
|
||||
/// </summary>
|
||||
Task<List<UploadedFile>> GetByCategoryAsync(FileCategory category, int pageNumber, int pageSize);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت فایلهای با وضعیت خاص
|
||||
/// </summary>
|
||||
Task<List<UploadedFile>> GetByStatusAsync(FileStatus status, int pageNumber, int pageSize);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت فایلهای یک Reference خاص
|
||||
/// </summary>
|
||||
Task<List<UploadedFile>> GetByReferenceAsync(string entityType, string entityId);
|
||||
|
||||
/// <summary>
|
||||
/// جستجو در فایلها بر اساس نام
|
||||
/// </summary>
|
||||
Task<List<UploadedFile>> SearchByNameAsync(string searchTerm, int pageNumber, int pageSize);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تعداد کل فایلهای یک کاربر
|
||||
/// </summary>
|
||||
Task<int> GetUserFilesCountAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت مجموع حجم فایلهای یک کاربر (به بایت)
|
||||
/// </summary>
|
||||
Task<long> GetUserTotalFileSizeAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت فایلهای منقضی شده برای پاکسازی
|
||||
/// </summary>
|
||||
Task<List<UploadedFile>> GetExpiredFilesAsync(DateTime olderThan);
|
||||
|
||||
/// <summary>
|
||||
/// اضافه کردن فایل جدید
|
||||
/// </summary>
|
||||
Task<UploadedFile> AddAsync(UploadedFile file);
|
||||
|
||||
/// <summary>
|
||||
/// بهروزرسانی فایل
|
||||
/// </summary>
|
||||
Task UpdateAsync(UploadedFile file);
|
||||
|
||||
/// <summary>
|
||||
/// حذف فیزیکی فایل (فقط برای cleanup)
|
||||
/// </summary>
|
||||
Task DeleteAsync(UploadedFile file);
|
||||
|
||||
/// <summary>
|
||||
/// ذخیره تغییرات
|
||||
/// </summary>
|
||||
Task<int> SaveChangesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// بررسی وجود فایل
|
||||
/// </summary>
|
||||
Task<bool> ExistsAsync(Guid fileId);
|
||||
|
||||
/// <summary>
|
||||
/// بررسی وجود فایل با نام یکتا
|
||||
/// </summary>
|
||||
Task<bool> ExistsByUniqueFileNameAsync(string uniqueFileName);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -20,7 +20,7 @@ public class ProjectTask : ProjectHierarchyNode
|
||||
{
|
||||
PhaseId = phaseId;
|
||||
_sections = new List<TaskSection>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,27 @@ public class TaskSection : EntityBase<Guid>
|
||||
return TimeSpan.FromTicks(_activities.Sum(a => a.GetTimeSpent().Ticks));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه درصد پیشرفت بر اساس زمان مصرف شده به تایم برآورد شده
|
||||
/// اگر وضعیت Completed باشد، همیشه 100 درصد برمیگرداند
|
||||
/// </summary>
|
||||
public double GetProgressPercentage()
|
||||
{
|
||||
// اگر تسک کامل شده، همیشه 100 درصد
|
||||
if (Status == TaskSectionStatus.Completed)
|
||||
return 100.0;
|
||||
|
||||
// اگر تایم برآورد شده صفر است، درصد صفر است
|
||||
if (FinalEstimatedHours.TotalHours <= 0)
|
||||
return 0.0;
|
||||
|
||||
var timeSpent = GetTotalTimeSpent();
|
||||
var percentage = (timeSpent.TotalMinutes / FinalEstimatedHours.TotalMinutes) * 100.0;
|
||||
|
||||
// محدود کردن درصد به 100 (در صورتی که زمان مصرف شده بیشتر از تخمین باشد)
|
||||
return Math.Min(percentage, 100.0);
|
||||
}
|
||||
|
||||
public bool IsCompleted()
|
||||
{
|
||||
return Status == TaskSectionStatus.Completed;
|
||||
@@ -249,7 +270,7 @@ public class TaskSection : EntityBase<Guid>
|
||||
// متوقف کردن فعالیت با EndDate دقیق شده
|
||||
activeActivity.StopWorkWithSpecificTime(adjustedEndDate, "متوقف خودکار - بیش از تایم تعیین شده");
|
||||
|
||||
UpdateStatus(TaskSectionStatus.Incomplete);
|
||||
UpdateStatus(TaskSectionStatus.PendingForCompletion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// وضعیت تکلیف دهی برای بخشهای مختلف پروژه
|
||||
/// </summary>
|
||||
public enum AssignmentStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// تعیین تکلیف نشده
|
||||
/// </summary>
|
||||
Unassigned = 0,
|
||||
|
||||
/// <summary>
|
||||
/// تعیین تکلیف شده
|
||||
/// </summary>
|
||||
Assigned = 1,
|
||||
|
||||
/// <summary>
|
||||
/// فقط کاربر تعیین شده
|
||||
/// </summary>
|
||||
UserOnly = 2,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
|
||||
/// <summary>
|
||||
/// اولویت تسک
|
||||
/// </summary>
|
||||
public enum TaskPriority
|
||||
public enum ProjectTaskPriority
|
||||
{
|
||||
/// <summary>
|
||||
/// پایین
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public interface IProjectTaskRepository : IRepository<Guid, ProjectTask>
|
||||
/// <summary>
|
||||
/// Get tasks by priority
|
||||
/// </summary>
|
||||
Task<List<ProjectTask>> GetByPriorityAsync(ProjectAgg.Enums.TaskPriority priority);
|
||||
Task<List<ProjectTask>> GetByPriorityAsync(ProjectAgg.Enums.ProjectTaskPriority priority);
|
||||
|
||||
/// <summary>
|
||||
/// Get tasks assigned to user
|
||||
|
||||
@@ -13,4 +13,8 @@ public interface ITaskSectionRepository: IRepository<Guid,TaskSection>
|
||||
|
||||
Task<List<TaskSection>> GetAssignedToUserAsync(long userId);
|
||||
Task<List<TaskSection>> GetActiveSectionsIncludeAllAsync(CancellationToken cancellationToken);
|
||||
Task<bool> HasUserAnyInProgressSectionAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
// جدید: دریافت سکشنهایی که هنوز Completed یا PendingForCompletion نشدهاند با اطلاعات کامل
|
||||
Task<List<TaskSection>> GetAllNotCompletedOrPendingIncludeAllAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// پیام چت تسک - Aggregate Root
|
||||
/// هر کسی که به تسک دسترسی داشته باشد میتواند پیامها را ببیند و ارسال کند
|
||||
/// نیازی به مدیریت گروه و ممبر نیست چون دسترسی از طریق خود تسک کنترل میشود
|
||||
/// </summary>
|
||||
public class TaskChatMessage : EntityBase<Guid>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// نوع پیام در چت تسک
|
||||
/// </summary>
|
||||
public enum MessageType
|
||||
{
|
||||
Text = 1, // پیام متنی
|
||||
File = 2, // فایل (اسناد، PDF، و غیره)
|
||||
Image = 3, // تصویر
|
||||
Voice = 4, // پیام صوتی
|
||||
Video = 5, // ویدیو
|
||||
Note = 6, // ✅ یادداشت سیستم (برای زمان اضافی و اطلاعات خودکار)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
|
||||
|
||||
namespace GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository برای مدیریت پیامهای چت تسک
|
||||
/// </summary>
|
||||
public interface ITaskChatMessageRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// دریافت پیام بر اساس شناسه
|
||||
/// </summary>
|
||||
Task<TaskChatMessage?> GetByIdAsync(Guid messageId);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت لیست پیامهای یک تسک (با صفحهبندی)
|
||||
/// </summary>
|
||||
Task<List<TaskChatMessage>> GetTaskMessagesAsync(Guid taskId, int pageNumber, int pageSize);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تعداد کل پیامهای یک تسک
|
||||
/// </summary>
|
||||
Task<int> GetTaskMessageCountAsync(Guid taskId);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت پیامهای پین شده یک تسک
|
||||
/// </summary>
|
||||
Task<List<TaskChatMessage>> GetPinnedMessagesAsync(Guid taskId);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت آخرین پیام یک تسک
|
||||
/// </summary>
|
||||
Task<TaskChatMessage?> GetLastMessageAsync(Guid taskId);
|
||||
|
||||
/// <summary>
|
||||
/// جستجو در پیامهای یک تسک
|
||||
/// </summary>
|
||||
Task<List<TaskChatMessage>> SearchMessagesAsync(Guid taskId, string searchText, int pageNumber, int pageSize);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت پیامهای یک کاربر خاص در یک تسک
|
||||
/// </summary>
|
||||
Task<List<TaskChatMessage>> GetUserMessagesAsync(Guid taskId, long userId, int pageNumber, int pageSize);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت پیامهای با فایل (تصویر، ویدیو، فایل و...) - پیامهایی که FileId دارند
|
||||
/// </summary>
|
||||
Task<List<TaskChatMessage>> GetMediaMessagesAsync(Guid taskId, int pageNumber, int pageSize);
|
||||
|
||||
/// <summary>
|
||||
/// اضافه کردن پیام جدید
|
||||
/// </summary>
|
||||
Task<TaskChatMessage> AddAsync(TaskChatMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// بهروزرسانی پیام
|
||||
/// </summary>
|
||||
Task UpdateAsync(TaskChatMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// حذف فیزیکی پیام (در صورت نیاز - معمولاً استفاده نمیشود)
|
||||
/// </summary>
|
||||
Task DeleteAsync(TaskChatMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// ذخیره تغییرات
|
||||
/// </summary>
|
||||
Task<int> SaveChangesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// بررسی وجود پیام
|
||||
/// </summary>
|
||||
Task<bool> ExistsAsync(Guid messageId);
|
||||
}
|
||||
|
||||
@@ -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<IUserRefreshTokenRepository, UserRefreshTokenRepository>();
|
||||
|
||||
// File Management & Task Chat
|
||||
services.AddScoped<IUploadedFileRepository, Persistence.Repositories.FileManagement.UploadedFileRepository>();
|
||||
services.AddScoped<ITaskChatMessageRepository, Persistence.Repositories.TaskChat.TaskChatMessageRepository>();
|
||||
|
||||
// File Storage Services
|
||||
services.AddScoped<IFileStorageService, Services.FileManagement.LocalFileStorageService>();
|
||||
services.AddScoped<IThumbnailGeneratorService, Services.FileManagement.ThumbnailGeneratorService>();
|
||||
|
||||
// JWT Settings
|
||||
services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));
|
||||
|
||||
|
||||
@@ -15,11 +15,21 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<!--<PackageReference Include="System.Text.Encodings.Web" Version="10.0.0" />-->
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Application\GozareshgirProgramManager.Application\GozareshgirProgramManager.Application.csproj" />
|
||||
<ProjectReference Include="..\..\Domain\GozareshgirProgramManager.Domain\GozareshgirProgramManager.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions">
|
||||
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.AspNetCore.Hosting.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Extensions.Hosting.Abstractions">
|
||||
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.Extensions.Hosting.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GozareshgirProgramManager.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class addtaskchatuploadedfile : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TaskChatMessages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TaskId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
SenderUserId = table.Column<long>(type: "bigint", nullable: false),
|
||||
MessageType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
TextContent = table.Column<string>(type: "nvarchar(4000)", maxLength: 4000, nullable: true),
|
||||
FileId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
ReplyToMessageId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsEdited = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
EditedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeletedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
IsPinned = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
PinnedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
PinnedByUserId = table.Column<long>(type: "bigint", nullable: true),
|
||||
CreationDate = table.Column<DateTime>(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<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
OriginalFileName = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
UniqueFileName = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
FileExtension = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
FileSizeBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||
MimeType = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
FileType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
Category = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
StorageProvider = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
StoragePath = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
StorageUrl = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
ThumbnailUrl = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
UploadedByUserId = table.Column<long>(type: "bigint", nullable: false),
|
||||
UploadDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
Status = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
ImageWidth = table.Column<int>(type: "int", nullable: true),
|
||||
ImageHeight = table.Column<int>(type: "int", nullable: true),
|
||||
DurationSeconds = table.Column<int>(type: "int", nullable: true),
|
||||
VirusScanDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
IsVirusScanPassed = table.Column<bool>(type: "bit", nullable: true),
|
||||
VirusScanResult = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeletedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
DeletedByUserId = table.Column<long>(type: "bigint", nullable: true),
|
||||
ReferenceEntityType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
ReferenceEntityId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
CreationDate = table.Column<DateTime>(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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "TaskChatMessages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UploadedFiles");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,131 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations
|
||||
b.ToTable("Customers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GozareshgirProgramManager.Domain.FileManagementAgg.Entities.UploadedFile", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long?>("DeletedByUserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("DeletedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("DurationSeconds")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("FileExtension")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<long>("FileSizeBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("FileType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<int?>("ImageHeight")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("ImageWidth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool?>("IsVirusScanPassed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("OriginalFileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("ReferenceEntityId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("ReferenceEntityType")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("StoragePath")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<string>("StorageProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<string>("ThumbnailUrl")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<string>("UniqueFileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<DateTime>("UploadDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long>("UploadedByUserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("VirusScanDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
@@ -495,6 +620,81 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations
|
||||
b.ToTable("Skills", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("DeletedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("EditedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("FileId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("IsPinned")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("MessageType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<long?>("PinnedByUserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("PinnedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("ReplyToMessageId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<long>("SenderUserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid>("TaskId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("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<long>("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 =>
|
||||
|
||||
@@ -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<Role> Roles { get; set; } = null!;
|
||||
|
||||
public DbSet<Skill> Skills { get; set; } = null!;
|
||||
|
||||
// File Management
|
||||
public DbSet<UploadedFile> UploadedFiles { get; set; } = null!;
|
||||
|
||||
// Task Chat
|
||||
public DbSet<TaskChatMessage> TaskChatMessages { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ProgramManagerDbContext).Assembly);
|
||||
|
||||
@@ -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<TaskChatMessage>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TaskChatMessage> 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<string>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -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<UploadedFile>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UploadedFile> 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<string>()
|
||||
.HasMaxLength(50);
|
||||
|
||||
builder.Property(x => x.Category)
|
||||
.IsRequired()
|
||||
.HasConversion<string>()
|
||||
.HasMaxLength(100);
|
||||
|
||||
// ذخیرهسازی
|
||||
builder.Property(x => x.StorageProvider)
|
||||
.IsRequired()
|
||||
.HasConversion<string>()
|
||||
.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<string>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -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<UploadedFile?> GetByIdAsync(Guid fileId)
|
||||
{
|
||||
return await _context.UploadedFiles
|
||||
.FirstOrDefaultAsync(x => x.Id == fileId);
|
||||
}
|
||||
|
||||
public async Task<UploadedFile?> GetByUniqueFileNameAsync(string uniqueFileName)
|
||||
{
|
||||
return await _context.UploadedFiles
|
||||
.FirstOrDefaultAsync(x => x.UniqueFileName == uniqueFileName);
|
||||
}
|
||||
|
||||
public async Task<List<UploadedFile>> 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<List<UploadedFile>> 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<List<UploadedFile>> 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<List<UploadedFile>> 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<List<UploadedFile>> 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<int> GetUserFilesCountAsync(long userId)
|
||||
{
|
||||
return await _context.UploadedFiles
|
||||
.CountAsync(x => x.UploadedByUserId == userId);
|
||||
}
|
||||
|
||||
public async Task<long> GetUserTotalFileSizeAsync(long userId)
|
||||
{
|
||||
return await _context.UploadedFiles
|
||||
.Where(x => x.UploadedByUserId == userId)
|
||||
.SumAsync(x => x.FileSizeBytes);
|
||||
}
|
||||
|
||||
public async Task<List<UploadedFile>> GetExpiredFilesAsync(DateTime olderThan)
|
||||
{
|
||||
return await _context.UploadedFiles
|
||||
.IgnoreQueryFilters() // Include deleted files
|
||||
.Where(x => x.IsDeleted && x.DeletedDate < olderThan)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<UploadedFile> 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<int> SaveChangesAsync()
|
||||
{
|
||||
return await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(Guid fileId)
|
||||
{
|
||||
return await _context.UploadedFiles
|
||||
.AnyAsync(x => x.Id == fileId);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsByUniqueFileNameAsync(string uniqueFileName)
|
||||
{
|
||||
return await _context.UploadedFiles
|
||||
.AnyAsync(x => x.UniqueFileName == uniqueFileName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public class ProjectPhaseRepository : RepositoryBase<Guid, ProjectPhase>, IProje
|
||||
public Task<ProjectPhase?> GetWithTasksAsync(Guid phaseId)
|
||||
{
|
||||
return _context.ProjectPhases
|
||||
.Include(x=>x.PhaseSections)
|
||||
.Include(p => p.Tasks)
|
||||
.ThenInclude(t => t.Sections)
|
||||
.ThenInclude(s => s.Skill)
|
||||
|
||||
@@ -58,7 +58,7 @@ public class ProjectTaskRepository : RepositoryBase<Guid, ProjectTask>, IProject
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public Task<List<ProjectTask>> GetByPriorityAsync(TaskPriority priority)
|
||||
public Task<List<ProjectTask>> GetByPriorityAsync(ProjectTaskPriority priority)
|
||||
{
|
||||
return _context.ProjectTasks
|
||||
.Where(t => t.Priority == priority)
|
||||
|
||||
@@ -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<TaskChatMessage?> GetByIdAsync(Guid messageId)
|
||||
{
|
||||
return await _context.TaskChatMessages
|
||||
.Include(x => x.ReplyToMessage)
|
||||
.FirstOrDefaultAsync(x => x.Id == messageId);
|
||||
}
|
||||
|
||||
public async Task<List<TaskChatMessage>> 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<int> GetTaskMessageCountAsync(Guid taskId)
|
||||
{
|
||||
return await _context.TaskChatMessages
|
||||
.CountAsync(x => x.TaskId == taskId);
|
||||
}
|
||||
|
||||
public async Task<List<TaskChatMessage>> 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<TaskChatMessage?> GetLastMessageAsync(Guid taskId)
|
||||
{
|
||||
return await _context.TaskChatMessages
|
||||
.Where(x => x.TaskId == taskId)
|
||||
.OrderByDescending(x => x.CreationDate)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<List<TaskChatMessage>> 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<List<TaskChatMessage>> 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<List<TaskChatMessage>> 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<TaskChatMessage> 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<int> SaveChangesAsync()
|
||||
{
|
||||
return await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(Guid messageId)
|
||||
{
|
||||
return await _context.TaskChatMessages
|
||||
.AnyAsync(x => x.Id == messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public class TaskSectionRepository:RepositoryBase<Guid,TaskSection>,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<Guid,TaskSection>,ITaskSection
|
||||
.Include(x => x.AdditionalTimes)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> HasUserAnyInProgressSectionAsync(long userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.TaskSections
|
||||
.AnyAsync(x => x.CurrentAssignedUserId == userId && x.Status == TaskSectionStatus.InProgress,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<List<TaskSection>> GetAllNotCompletedOrPendingIncludeAllAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return _context.TaskSections
|
||||
.Where(x => x.Status != TaskSectionStatus.Completed && x.Status != TaskSectionStatus.PendingForCompletion)
|
||||
.Include(x => x.Activities)
|
||||
.Include(x => x.AdditionalTimes)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس ذخیرهسازی فایل در سیستم فایل محلی
|
||||
/// </summary>
|
||||
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<Stream?> GetFileStreamAsync(string storagePath)
|
||||
{
|
||||
if (!File.Exists(storagePath))
|
||||
{
|
||||
return Task.FromResult<Stream?>(null);
|
||||
}
|
||||
|
||||
var stream = new FileStream(storagePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
return Task.FromResult<Stream?>(stream);
|
||||
}
|
||||
|
||||
public Task<bool> ExistsAsync(string storagePath)
|
||||
{
|
||||
return Task.FromResult(File.Exists(storagePath));
|
||||
}
|
||||
|
||||
public string GetFileUrl(string storagePath)
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(_uploadBasePath, storagePath)
|
||||
.Replace("\\", "/");
|
||||
return $"{_baseUrl}/{relativePath}";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس تولید thumbnail با استفاده از ImageSharp
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت مسیر فیزیکی و URL برای thumbnail بر اساس category
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<ActionResult<OperationResult<GetProjectSearchResponse>>> Search(
|
||||
[FromQuery] string search)
|
||||
{
|
||||
var searchQuery = new GetProjectSearchQuery(search);
|
||||
var res = await _mediator.Send(searchQuery);
|
||||
return res;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<OperationResult>> 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<ActionResult<OperationResult>> ApproveTaskSectionCompletion([FromBody] ApproveTaskSectionCompletionCommand command)
|
||||
{
|
||||
var res = await _mediator.Send(command);
|
||||
return res;
|
||||
}
|
||||
|
||||
[HttpPost("change-priority")]
|
||||
public async Task<ActionResult<OperationResult>> ChangePriority([FromBody] ChangeTaskPriorityCommand command)
|
||||
{
|
||||
var res = await _mediator.Send(command);
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// کنترلر مدیریت چت تسک
|
||||
/// </summary>
|
||||
public class TaskChatController : ProgramManagerBaseController
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public TaskChatController(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت لیست پیامهای یک تسک
|
||||
/// </summary>
|
||||
/// <param name="taskId">شناسه تسک</param>
|
||||
/// <param name="page">صفحه (پیشفرض: 1)</param>
|
||||
/// <param name="pageSize">تعداد در هر صفحه (پیشفرض: 50)</param>
|
||||
[HttpGet("{taskId:guid}/messages")]
|
||||
public async Task<ActionResult<OperationResult<PaginationResult<MessageDto>>>> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت پیامهای پین شده یک تسک
|
||||
/// </summary>
|
||||
/// <param name="taskId">شناسه تسک</param>
|
||||
[HttpGet("{taskId:guid}/messages/pinned")]
|
||||
public async Task<ActionResult<OperationResult<List<MessageDto>>>> GetPinnedMessages(Guid taskId)
|
||||
{
|
||||
var query = new GetPinnedMessagesQuery(taskId);
|
||||
var result = await _mediator.Send(query);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// جستجو در پیامهای یک تسک
|
||||
/// </summary>
|
||||
/// <param name="taskId">شناسه تسک</param>
|
||||
/// <param name="search">متن جستجو</param>
|
||||
/// <param name="page">صفحه</param>
|
||||
/// <param name="pageSize">تعداد در هر صفحه</param>
|
||||
[HttpGet("{taskId:guid}/messages/search")]
|
||||
public async Task<ActionResult<OperationResult<List<MessageDto>>>> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ارسال پیام جدید (با یا بدون فایل)
|
||||
/// </summary>
|
||||
[HttpPost("messages")]
|
||||
public async Task<ActionResult<OperationResult<MessageDto>>> SendMessage(
|
||||
[FromForm] SendMessageCommand command)
|
||||
{
|
||||
var result = await _mediator.Send(command);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ویرایش پیام (فقط متن)
|
||||
/// </summary>
|
||||
/// <param name="messageId">شناسه پیام</param>
|
||||
/// <param name="request">محتوای جدید</param>
|
||||
[HttpPut("messages/{messageId:guid}")]
|
||||
public async Task<ActionResult<OperationResult>> EditMessage(
|
||||
Guid messageId,
|
||||
[FromBody] EditMessageRequest request)
|
||||
{
|
||||
var command = new EditMessageCommand(messageId, request.NewTextContent);
|
||||
var result = await _mediator.Send(command);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// حذف پیام
|
||||
/// </summary>
|
||||
/// <param name="messageId">شناسه پیام</param>
|
||||
[HttpDelete("messages/{messageId:guid}")]
|
||||
public async Task<ActionResult<OperationResult>> DeleteMessage(Guid messageId)
|
||||
{
|
||||
var command = new DeleteMessageCommand(messageId);
|
||||
var result = await _mediator.Send(command);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// پین کردن پیام
|
||||
/// </summary>
|
||||
/// <param name="messageId">شناسه پیام</param>
|
||||
[HttpPost("messages/{messageId:guid}/pin")]
|
||||
public async Task<ActionResult<OperationResult>> PinMessage(Guid messageId)
|
||||
{
|
||||
var command = new PinMessageCommand(messageId);
|
||||
var result = await _mediator.Send(command);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// برداشتن پین پیام
|
||||
/// </summary>
|
||||
/// <param name="messageId">شناسه پیام</param>
|
||||
[HttpPost("messages/{messageId:guid}/unpin")]
|
||||
public async Task<ActionResult<OperationResult>> 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;
|
||||
}
|
||||
|
||||
@@ -916,6 +916,17 @@ public class institutionContractController : AdminBaseController
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
$"قرارداد های مالی.xlsx");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تنظیم وضعیت ارسال قرارداد
|
||||
/// </summary>
|
||||
[HttpPost("set-is-sent")]
|
||||
public async Task<ActionResult<OperationResult>> SetIsSent([FromBody] SetInstitutionContractSendFlagRequest request)
|
||||
{
|
||||
var result = await _institutionContractApplication.SetContractSendFlag(request);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class InstitutionContractCreationGetRepresentativeIdResponse
|
||||
@@ -969,4 +980,4 @@ public class VerifyCodeRequest
|
||||
{
|
||||
public long ContractingPartyId { get; set; }
|
||||
public string verifyCode { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<RazorCompileOnBuild>true</RazorCompileOnBuild>
|
||||
<UserSecretsId>a6049acf-0286-4947-983a-761d06d65f36</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 312 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user