Merge branch 'master' into Main

This commit is contained in:
2026-01-07 10:25:52 +03:30
14 changed files with 497 additions and 358 deletions

View File

@@ -6585,268 +6585,275 @@ 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
// {
// Id = x.id,
// ContractingPartyId = x.ContractingPartyId,
// ContractingPartyName = x.ContractingPartyName,
// ContractStartGr = x.ContractStartGr,
// ContractStartFa = x.ContractStartFa,
// ContractEndGr = x.ContractEndGr,
// ContractEndFa = x.ContractEndFa,
// IsActiveString = x.IsActiveString,
// ContractAmountDouble = x.ContractAmount,
// OfficialCompany = x.OfficialCompany,
// IsInstallment = x.IsInstallment,
// VerificationStatus = x.VerificationStatus,
// SigningType = x.SigningType,
// }).Where(x => x.IsActiveString == "blue" &&
// x.ContractAmountDouble > 0).GroupBy(x => x.ContractingPartyId).Select(x => x.First()).ToListAsync();
var institutionContracts = await _context.InstitutionContractSet.AsQueryable().Select(x => new InstitutionContractViewModel
{
Id = x.id,
ContractingPartyId = x.ContractingPartyId,
ContractingPartyName = x.ContractingPartyName,
ContractStartGr = x.ContractStartGr,
ContractStartFa = x.ContractStartFa,
ContractEndGr = x.ContractEndGr,
ContractEndFa = x.ContractEndFa,
IsActiveString = x.IsActiveString,
ContractAmountDouble = x.ContractAmount,
OfficialCompany = x.OfficialCompany,
IsInstallment = x.IsInstallment,
VerificationStatus = x.VerificationStatus,
SigningType = x.SigningType,
}).Where(x => x.IsActiveString == "blue" &&
x.ContractAmountDouble > 0).GroupBy(x => x.ContractingPartyId).Select(x => x.First()).ToListAsync();
// // قرارداد هایی که بطور یکجا پرداخت شده اند
// var paidInFull = institutionContracts.Where(x =>
// x.SigningType != InstitutionContractSigningType.Legacy && x.IsInstallment == false && x.SigningType != null).ToList();
var institutionContractsIds = institutionContracts.Select(x => x.id).ToList();
// //حذف قراداد هایی که یکجا پرداخت شده اند از لیست ایجاد سند ماهانه
// institutionContracts = institutionContracts.Except(paidInFull).ToList();
// قرارداد هایی که بطور یکجا پرداخت شده اند
var paidInFull = institutionContracts.Where(x =>
x.SigningType != InstitutionContractSigningType.Legacy && x.IsInstallment == false && x.SigningType != null).ToList();
// var contractingPartyList = await _context.PersonalContractingParties
// .Where(x => institutionContracts.Select(ins => ins.ContractingPartyId).Contains(x.id)).ToListAsync();
//حذف قراداد هایی که یکجا پرداخت شده اند از لیست ایجاد سند ماهانه
institutionContracts = institutionContracts.Except(paidInFull).ToList();
// var financialStatmentList = await _context.FinancialStatments.AsSplitQuery()
// .Where(x => institutionContracts.Select(ins => ins.ContractingPartyId).Contains(x.ContractingPartyId))
// .Include(x => x.FinancialTransactionList).Where(x => x.FinancialTransactionList.Count > 0).ToListAsync();
var contractingPartyList = await _context.PersonalContractingParties
.Where(x => institutionContracts.Select(ins => ins.ContractingPartyId).Contains(x.id)).ToListAsync();
// var phoneNumberList = await _context.InstitutionContractContactInfos
// .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
// .Where(x => x.TypeOfSms == "اقدام قضایی").ToListAsync();
// var warningSentSms = await _context.SmsResults.Where(x => x.TypeOfSms.Contains("هشدار")).ToListAsync();
var financialStatmentList = await _context.FinancialStatments.AsSplitQuery()
.Where(x => institutionContracts.Select(ins => ins.ContractingPartyId).Contains(x.ContractingPartyId))
.Include(x => x.FinancialTransactionList).Where(x => x.FinancialTransactionList.Count > 0).ToListAsync();
// var oldInstitutionContract = institutionContracts.Where(x => x.IsInstallment == false).ToList();
// var electronicInstitutionContract = institutionContracts.Where(x => x.IsInstallment == true).ToList();
var phoneNumberList = await _context.InstitutionContractContactInfos
.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
.Where(x => x.TypeOfSms == "اقدام قضایی").ToListAsync();
var warningSentSms = await _context.SmsResults.Where(x => x.TypeOfSms.Contains("هشدار")).ToListAsync();
// foreach (var item in oldInstitutionContract)
// {
// try
// {
// var contractingParty = GetDetails(item.ContractingPartyId);
// bool hasLegalActionSentSms = legalActionSentSms.Any(x => x.InstitutionContractId == item.Id);
var oldInstitutionContract = institutionContracts.Where(x => x.IsInstallment == false).ToList();
var electronicInstitutionContract = institutionContracts.Where(x => x.IsInstallment == true).ToList();
// if (!string.IsNullOrWhiteSpace(contractingParty.LName) && !hasLegalActionSentSms)
// {
// //Thread.Sleep(500);
// var partyName = contractingParty.LName;
foreach (var item in oldInstitutionContract)
{
try
{
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 (partyName.Length > 25) partyName = $"{partyName.Substring(0, 25)}";
if (!string.IsNullOrWhiteSpace(contractingParty.LName) && !hasLegalActionSentSms && now.Date <= endOfContractNextMonthEnd.Date)
{
//Thread.Sleep(500);
var partyName = contractingParty.LName;
// var isLegal = contractingParty.IsLegal == "حقوقی" ? true : false;
// var isBlock = contractingParty.IsBlock == "true" ? true : false;
// var isActive = contractingParty.IsActiveString == "true" ? true : false;
// if (!string.IsNullOrWhiteSpace(contractingParty.IsActiveString))
// {
// var hasFinancialStatement =
// financialStatmentList.Any(x => x.ContractingPartyId == item.ContractingPartyId);
if (partyName.Length > 25) partyName = $"{partyName.Substring(0, 25)}";
// var hasPhonNumber = phoneNumberList.Any(x => x.InstitutionContractId == item.Id);
var isLegal = contractingParty.IsLegal == "حقوقی" ? true : false;
var isBlock = contractingParty.IsBlock == "true" ? true : false;
var isActive = contractingParty.IsActiveString == "true" ? true : false;
if (!string.IsNullOrWhiteSpace(contractingParty.IsActiveString))
{
var hasFinancialStatement =
financialStatmentList.Any(x => x.ContractingPartyId == item.ContractingPartyId);
// if (hasFinancialStatement && hasPhonNumber)
// {
// var phoneNumbers = phoneNumberList.Where(x => x.InstitutionContractId == item.Id)
// .Select(x => new CreateContactInfo
// {
// PhoneType = x.PhoneType,
// PhoneNumber = x.PhoneNumber,
var hasPhonNumber = phoneNumberList.Any(x => x.InstitutionContractId == item.Id);
// InstitutionContractId = x.InstitutionContractId,
// SendSms = x.SendSms
// }).Where(x => x.PhoneNumber.Length == 11).ToList();
if (hasFinancialStatement && hasPhonNumber)
{
var phoneNumbers = phoneNumberList.Where(x => x.InstitutionContractId == item.Id)
.Select(x => new CreateContactInfo
{
PhoneType = x.PhoneType,
PhoneNumber = x.PhoneNumber,
// var transactions = financialStatmentList.FirstOrDefault(x =>
// x.ContractingPartyId == item.ContractingPartyId);
InstitutionContractId = x.InstitutionContractId,
SendSms = x.SendSms
}).Where(x => x.PhoneNumber.Length == 11).ToList();
var transactions = financialStatmentList.FirstOrDefault(x =>
x.ContractingPartyId == item.ContractingPartyId);
// var debtor = transactions.FinancialTransactionViewModels.Sum(x => x.Deptor);
// var creditor = transactions.FinancialTransactionViewModels.Sum(x => x.Creditor);
var debtor = transactions.FinancialTransactionViewModels.Sum(x => x.Deptor);
var creditor = transactions.FinancialTransactionViewModels.Sum(x => x.Creditor);
// var id = $"{item.ContractingPartyId}";
// var aprove = $"{transactions.Id}";
// var balance = debtor - creditor;
// if (balance > 0) // اگر بدهکار بود
// {
var id = $"{item.ContractingPartyId}";
var aprove = $"{transactions.Id}";
var balance = debtor - creditor;
// if (isLegal)
// {
// if (item.OfficialCompany == "Official") // حقوقی بدهکار رسمی
// {
// var balanceToMoney = balance.ToMoney();
if (balance > 0) // اگر بدهکار بود
{
// foreach (var number in phoneNumbers)
// {
// var isLastAlarmSend = _context.SmsResults.Any(x => (
// x.ContractingPatyId == contractingParty.Id &&
// x.Mobile == number.PhoneNumber) && (x.TypeOfSms == "اقدام قضایی" || x.TypeOfSms == "هشدار دوم"));
if (isLegal)
{
if (item.OfficialCompany == "Official") // حقوقی بدهکار رسمی
{
var balanceToMoney = balance.ToMoney();
// if (!string.IsNullOrWhiteSpace(number.PhoneNumber) &&
// number.PhoneNumber.Length == 11 && !isSend && !isLastAlarmSend)
// {
foreach (var number in phoneNumbers)
{
// var smsResult = _smsService.MonthlyBill(number.PhoneNumber, 608443,
// partyName,
// balanceToMoney, id, aprove);
// Thread.Sleep(1000);
// if (smsResult.IsSuccedded)
// {
// var createSmsResult = new SmsResult(smsResult.MessageId,
// smsResult.Message, typeOfSms, partyName, number.PhoneNumber,
// item.ContractingPartyId, item.Id);
// _smsResultRepository.Create(createSmsResult);
// _smsResultRepository.SaveChanges();
// Thread.Sleep(1000);
// }
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)
{
// }
// }
var smsResult = _smsService.MonthlyBill(number.PhoneNumber, 608443,
partyName,
balanceToMoney, id, aprove);
Thread.Sleep(1000);
if (smsResult.IsSuccedded)
{
var createSmsResult = new SmsResult(smsResult.MessageId,
smsResult.Message, typeOfSms, partyName, number.PhoneNumber,
item.ContractingPartyId, item.Id);
_smsResultRepository.Create(createSmsResult);
_smsResultRepository.SaveChanges();
Thread.Sleep(1000);
}
}
}
// }
// else if (item.OfficialCompany == "NotOfficial") // حقوقی بدهکار غیر رسمی
// {
// var balanceToMoney = balance.ToMoney();
// foreach (var number in phoneNumbers)
// {
// var isSend = _context.SmsResults.Any(x =>
// x.ContractingPatyId == contractingParty.Id &&
// x.Mobile == number.PhoneNumber && x.TypeOfSms == typeOfSms);
// var isLastAlarmSend = _context.SmsResults.Any(x => (
// x.ContractingPatyId == contractingParty.Id &&
// x.Mobile == number.PhoneNumber) && (x.TypeOfSms == "اقدام قضایی" || x.TypeOfSms == "هشدار دوم"));
// if (!string.IsNullOrWhiteSpace(number.PhoneNumber) &&
// number.PhoneNumber.Length == 11 && !isSend && !isLastAlarmSend)
// {
}
else if (item.OfficialCompany == "NotOfficial") // حقوقی بدهکار غیر رسمی
{
var balanceToMoney = balance.ToMoney();
foreach (var number in phoneNumbers)
{
var isSend = _context.SmsResults.Any(x =>
x.ContractingPatyId == contractingParty.Id &&
x.Mobile == number.PhoneNumber && x.TypeOfSms == typeOfSms);
var isLastAlarmSend = _context.SmsResults.Any(x => (
x.ContractingPatyId == contractingParty.Id &&
x.Mobile == number.PhoneNumber) && (x.TypeOfSms == "اقدام قضایی" || x.TypeOfSms == "هشدار دوم"));
if (!string.IsNullOrWhiteSpace(number.PhoneNumber) &&
number.PhoneNumber.Length == 11 && !isSend && !isLastAlarmSend)
{
// var smsResult = _smsService.MonthlyBill(number.PhoneNumber, 351691,
// partyName,
// balanceToMoney, id, aprove);
// Thread.Sleep(1000);
// if (smsResult.IsSuccedded)
// {
// var createSmsResult = new SmsResult(smsResult.MessageId,
// smsResult.Message, typeOfSms, partyName, number.PhoneNumber,
// item.ContractingPartyId, item.Id);
// _smsResultRepository.Create(createSmsResult);
// _smsResultRepository.SaveChanges();
// Thread.Sleep(1000);
// }
var smsResult = _smsService.MonthlyBill(number.PhoneNumber, 351691,
partyName,
balanceToMoney, id, aprove);
Thread.Sleep(1000);
if (smsResult.IsSuccedded)
{
var createSmsResult = new SmsResult(smsResult.MessageId,
smsResult.Message, typeOfSms, partyName, number.PhoneNumber,
item.ContractingPartyId, item.Id);
_smsResultRepository.Create(createSmsResult);
_smsResultRepository.SaveChanges();
Thread.Sleep(1000);
}
// }
// }
}
}
// }
// }
// else
// {
// if (item.OfficialCompany == "Official") // حقیقی بدهکار رسمی
// {
// var balanceToMoney = balance.ToMoney();
}
}
else
{
if (item.OfficialCompany == "Official") // حقیقی بدهکار رسمی
{
var balanceToMoney = balance.ToMoney();
// foreach (var number in phoneNumbers)
// {
// var isSend = _context.SmsResults.Any(x =>
// x.ContractingPatyId == contractingParty.Id &&
// x.Mobile == number.PhoneNumber && x.TypeOfSms == typeOfSms);
// var isLastAlarmSend = _context.SmsResults.Any(x => (
// x.ContractingPatyId == contractingParty.Id &&
// x.Mobile == number.PhoneNumber) && (x.TypeOfSms == "اقدام قضایی" || x.TypeOfSms == "هشدار دوم"));
// if (!string.IsNullOrWhiteSpace(number.PhoneNumber) &&
// number.PhoneNumber.Length == 11 && !isSend && !isLastAlarmSend)
// {
foreach (var number in phoneNumbers)
{
var isSend = _context.SmsResults.Any(x =>
x.ContractingPatyId == contractingParty.Id &&
x.Mobile == number.PhoneNumber && x.TypeOfSms == typeOfSms);
var isLastAlarmSend = _context.SmsResults.Any(x => (
x.ContractingPatyId == contractingParty.Id &&
x.Mobile == number.PhoneNumber) && (x.TypeOfSms == "اقدام قضایی" || x.TypeOfSms == "هشدار دوم"));
if (!string.IsNullOrWhiteSpace(number.PhoneNumber) &&
number.PhoneNumber.Length == 11 && !isSend && !isLastAlarmSend)
{
// var smsResult = _smsService.MonthlyBill(number.PhoneNumber, 190430,
// partyName,
// balanceToMoney, id, aprove);
// Thread.Sleep(1000);
// if (smsResult.IsSuccedded)
// {
// var createSmsResult = new SmsResult(smsResult.MessageId,
// smsResult.Message, typeOfSms, partyName, number.PhoneNumber,
// item.ContractingPartyId, item.Id);
// _smsResultRepository.Create(createSmsResult);
// _smsResultRepository.SaveChanges();
// Thread.Sleep(1000);
// }
// }
// }
var smsResult = _smsService.MonthlyBill(number.PhoneNumber, 190430,
partyName,
balanceToMoney, id, aprove);
Thread.Sleep(1000);
if (smsResult.IsSuccedded)
{
var createSmsResult = new SmsResult(smsResult.MessageId,
smsResult.Message, typeOfSms, partyName, number.PhoneNumber,
item.ContractingPartyId, item.Id);
_smsResultRepository.Create(createSmsResult);
_smsResultRepository.SaveChanges();
Thread.Sleep(1000);
}
}
}
// }
// else if (item.OfficialCompany == "NotOfficial") // حقیقی بدهکار غیر رسمی
// {
// var balanceToMoney = balance.ToMoney();
}
else if (item.OfficialCompany == "NotOfficial") // حقیقی بدهکار غیر رسمی
{
var balanceToMoney = balance.ToMoney();
// foreach (var number in phoneNumbers)
// {
// var isSend = _context.SmsResults.Any(x =>
// x.ContractingPatyId == contractingParty.Id &&
// x.Mobile == number.PhoneNumber && x.TypeOfSms == typeOfSms);
// var isLastAlarmSend = _context.SmsResults.Any(x => (
// x.ContractingPatyId == contractingParty.Id &&
// x.Mobile == number.PhoneNumber) && (x.TypeOfSms == "اقدام قضایی" || x.TypeOfSms == "هشدار دوم"));
// if (!string.IsNullOrWhiteSpace(number.PhoneNumber) &&
// number.PhoneNumber.Length == 11 && !isSend && !isLastAlarmSend)
// {
// var smsResult = _smsService.MonthlyBill(number.PhoneNumber, 412829,
// partyName,
// balanceToMoney, id, aprove);
// Thread.Sleep(1000);
// if (smsResult.IsSuccedded)
// {
// var createSmsResult = new SmsResult(smsResult.MessageId,
// smsResult.Message, typeOfSms, partyName, number.PhoneNumber,
// item.ContractingPartyId, item.Id);
// _smsResultRepository.Create(createSmsResult);
// _smsResultRepository.SaveChanges();
// Thread.Sleep(1000);
// }
foreach (var number in phoneNumbers)
{
var isSend = _context.SmsResults.Any(x =>
x.ContractingPatyId == contractingParty.Id &&
x.Mobile == number.PhoneNumber && x.TypeOfSms == typeOfSms);
var isLastAlarmSend = _context.SmsResults.Any(x => (
x.ContractingPatyId == contractingParty.Id &&
x.Mobile == number.PhoneNumber) && (x.TypeOfSms == "اقدام قضایی" || x.TypeOfSms == "هشدار دوم"));
if (!string.IsNullOrWhiteSpace(number.PhoneNumber) &&
number.PhoneNumber.Length == 11 && !isSend && !isLastAlarmSend)
{
var smsResult = _smsService.MonthlyBill(number.PhoneNumber, 412829,
partyName,
balanceToMoney, id, aprove);
Thread.Sleep(1000);
if (smsResult.IsSuccedded)
{
var createSmsResult = new SmsResult(smsResult.MessageId,
smsResult.Message, typeOfSms, partyName, number.PhoneNumber,
item.ContractingPartyId, item.Id);
_smsResultRepository.Create(createSmsResult);
_smsResultRepository.SaveChanges();
Thread.Sleep(1000);
}
// }
// }
}
}
// }
// }
// }
// }
// }
// }
// }
// catch (Exception e)
// {
}
}
}
}
}
}
}
catch (Exception e)
{
// string name = item.ContractingPartyName.Length > 18 ? item.ContractingPartyName.Substring(0, 18) : item.ContractingPartyName;
// string errMess = $"{name}-خطا";
// _smsService.Alarm("09114221321", errMess);
// _logger.LogError(e, "BlueWarningSms");
// }
// }
//}
string name = item.ContractingPartyName.Length > 18 ? item.ContractingPartyName.Substring(0, 18) : item.ContractingPartyName;
string errMess = $"{name}-خطا";
_smsService.Alarm("09114221321", errMess);
_logger.LogError(e, "BlueWarningSms");
}
}
}
#endregion

View File

@@ -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; }

View File

@@ -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

View File

@@ -1,20 +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
{
}

View File

@@ -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,47 +18,47 @@ 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, totalTime) = await CalculateProjectPercentage(project, cancellationToken);
result.Add(new GetProjectListDto
result.Add(new GetProjectDto
{
Id = project.Id,
Name = project.Name,
@@ -68,28 +69,24 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
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, totalTime) = await CalculatePhasePercentage(phase, cancellationToken);
result.Add(new GetProjectListDto
result.Add(new GetPhaseDto
{
Id = phase.Id,
Name = phase.Name,
@@ -100,28 +97,87 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
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, totalTime) = await CalculateTaskPercentage(task, cancellationToken);
result.Add(new GetProjectListDto
var sections = await _context.TaskSections
.Include(s => s.Activities)
.Include(s => s.Skill)
.Where(s => s.TaskId == task.Id)
.ToListAsync(cancellationToken);
// جمع‌آوری تمام userId های مورد نیاز
var userIds = sections
.Where(s => s.CurrentAssignedUserId > 0)
.Select(s => s.CurrentAssignedUserId)
.Distinct()
.ToList();
// دریافت اطلاعات کاربران
var users = await _context.Users
.Where(u => userIds.Contains(u.Id))
.Select(u => new { u.Id, u.FullName })
.ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
// محاسبه SpentTime و RemainingTime
var spentTime = TimeSpan.FromTicks(sections.Sum(s => s.Activities.Sum(a => a.GetTimeSpent().Ticks)));
var remainingTime = totalTime - spentTime;
// ساخت section DTOs برای تمام Skills
var sectionDtos = allSkills.Select(skill =>
{
var section = sections.FirstOrDefault(s => s.SkillId == skill.Id);
if (section == null)
{
// اگر section وجود نداشت، یک DTO با وضعیت Unassigned برمی‌گردانیم
return new GetTaskSectionDto
{
Id = Guid.Empty,
SkillName = skill.Name ?? string.Empty,
SpentTime = TimeSpan.Zero,
TotalTime = TimeSpan.Zero,
Percentage = 0,
UserFullName = string.Empty,
AssignmentStatus = AssignmentStatus.Unassigned
};
}
// اگر section وجود داشت
return new GetTaskSectionDto
{
Id = section.Id,
SkillName = skill.Name ?? string.Empty,
SpentTime = TimeSpan.FromTicks(section.Activities.Sum(a => a.GetTimeSpent().Ticks)),
TotalTime = section.FinalEstimatedHours,
Percentage = (int)section.GetProgressPercentage(),
UserFullName = section.CurrentAssignedUserId > 0 && users.ContainsKey(section.CurrentAssignedUserId)
? users[section.CurrentAssignedUserId]
: string.Empty,
AssignmentStatus = GetAssignmentStatus(section)
};
}).ToList();
result.Add(new GetTaskDto
{
Id = task.Id,
Name = task.Name,
@@ -129,187 +185,175 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
ParentId = task.PhaseId,
Percentage = percentage,
TotalHours = (int)totalTime.TotalHours,
Minutes = totalTime.Minutes
Minutes = totalTime.Minutes,
SpentTime = spentTime,
RemainingTime = remainingTime,
Sections = sectionDtos,
});
}
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");
}
}
private async Task SetSkillFlagsForTasks(List<GetProjectListDto> projects, List<Guid> taskIds, 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");
// 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<(int Percentage, TimeSpan TotalTime)> CalculateProjectPercentage(Project project, CancellationToken cancellationToken)
{
// گرفتن تمام فازهای پروژه
var phases = await _context.ProjectPhases
.Where(ph => ph.ProjectId == project.Id)
.ToListAsync(cancellationToken);
if (!phases.Any())
return (0, TimeSpan.Zero);
// محاسبه درصد هر فاز و میانگین‌گیری
var phasePercentages = new List<int>();
var totalTime = TimeSpan.Zero;
foreach (var phase in phases)
{
var (phasePercentage, phaseTime) = await CalculatePhasePercentage(phase, cancellationToken);
phasePercentages.Add(phasePercentage);
totalTime += phaseTime;
}
var averagePercentage = phasePercentages.Any() ? (int)phasePercentages.Average() : 0;
return (averagePercentage, totalTime);
}
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, TimeSpan.Zero);
// محاسبه درصد هر تسک و میانگین‌گیری
var taskPercentages = new List<int>();
var totalTime = TimeSpan.Zero;
foreach (var task in tasks)
{
var (taskPercentage, taskTime) = await CalculateTaskPercentage(task, cancellationToken);
taskPercentages.Add(taskPercentage);
totalTime += taskTime;
}
var averagePercentage = taskPercentages.Any() ? (int)taskPercentages.Average() : 0;
return (averagePercentage, totalTime);
}
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, TimeSpan.Zero);
// محاسبه درصد هر سکشن و میانگین‌گیری
var sectionPercentages = new List<int>();
var totalTime = TimeSpan.Zero;
foreach (var section in sections)
{
var (sectionPercentage, sectionTime) = CalculateSectionPercentage(section);
sectionPercentages.Add(sectionPercentage);
totalTime += sectionTime;
}
var averagePercentage = sectionPercentages.Any() ? (int)sectionPercentages.Average() : 0;
return (averagePercentage, totalTime);
}
private static (int Percentage, TimeSpan TotalTime) CalculateSectionPercentage(TaskSection section)
{
// محاسبه کل زمان تخمین زده شده (اولیه + اضافی)
var totalEstimatedHours = section.FinalEstimatedHours.TotalHours;
return ((int)section.GetProgressPercentage(),section.FinalEstimatedHours);
}
// محاسبه کل زمان صرف شده از activities
var totalSpentTime = TimeSpan.FromHours(section.Activities.Sum(a => a.GetTimeSpent().TotalHours));
private static AssignmentStatus GetAssignmentStatus(TaskSection? section)
{
// تعیین تکلیف نشده: section وجود ندارد
if (section == null)
return AssignmentStatus.Unassigned;
if (totalEstimatedHours <= 0)
return (0, section.FinalEstimatedHours);
// بررسی وجود user
bool hasUser = section.CurrentAssignedUserId > 0;
// بررسی وجود time (InitialEstimatedHours بزرگتر از صفر باشد)
bool hasTime = section.InitialEstimatedHours > TimeSpan.Zero;
var totalSpentHours = totalSpentTime.TotalHours;
// تعیین تکلیف شده: هم user و هم time تعیین شده
if (hasUser && hasTime)
return AssignmentStatus.Assigned;
// محاسبه درصد (حداکثر 100%)
var percentage = (totalSpentHours / totalEstimatedHours) * 100;
return (Math.Min((int)Math.Round(percentage), 100), section.FinalEstimatedHours);
// فقط کاربر تعیین شده: user دارد ولی time ندارد
if (hasUser && !hasTime)
return AssignmentStatus.UserOnly;
// تعیین تکلیف نشده: نه user دارد نه time
return AssignmentStatus.Unassigned;
}
}

View File

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

View File

@@ -0,0 +1,31 @@
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 List<GetTaskSectionDto> Sections { get; init; }
}
public class GetTaskSectionDto
{
public Guid Id { get; init; }
public string SkillName { get; init; } = string.Empty;
public TimeSpan SpentTime { get; init; }
public TimeSpan TotalTime { get; init; }
public int Percentage { get; init; }
public string UserFullName{ get; init; } = string.Empty;
public AssignmentStatus AssignmentStatus { get; set; }
}

View File

@@ -106,6 +106,7 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
Progress = new ProjectProgressDto()
{
CompleteSecond = x.FinalEstimatedHours.TotalSeconds,
Percentage = (int)x.GetProgressPercentage(),
CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds),
Histories = mergedHistories
},

View File

@@ -18,6 +18,7 @@ public class ProjectBoardListResponse
public class ProjectProgressDto
{
public double CurrentSecond { get; set; }
public int Percentage { get; set; }
public double CompleteSecond { get; set; }
public List<ProjectProgressHistoryDto> Histories { get; set; }
}

View File

@@ -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);
@@ -79,22 +81,20 @@ 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();
var taskPercentage = (int)Math.Round(skills.Average(x => x.TimePercentage));
return new ProjectDeployBoardDetailTaskItem(
t.Name,
totalTime,
doneTime,
taskPercentage,
skills);
}).ToList();
@@ -104,7 +104,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);

View File

@@ -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);

View File

@@ -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;

View File

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

View File

@@ -380,13 +380,7 @@ builder.Host.UseSerilog((context, services, configuration) =>
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext();
// در محیط Development، EF Core Commands را هم لاگ می‌کنیم
if (context.HostingEnvironment.IsDevelopment())
{
logConfig
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Information);
}
logConfig.WriteTo.File(
path: Path.Combine(logDirectory, "gozareshgir_log.txt"),