This commit is contained in:
SamSys
2026-01-07 11:29:33 +03:30
79 changed files with 671 additions and 381 deletions

4
.gitignore vendored
View File

@@ -365,3 +365,7 @@ MigrationBackup/
/ServiceHost/appsettings.Development.json
/ServiceHost/appsettings.json
/ServiceHost/web.config
# Storage folder - ignore all uploaded files, thumbnails, and temporary files
ServiceHost/Storage

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

@@ -106,7 +106,8 @@ public class SendMessageCommandHandler : IBaseCommandHandler<SendMessageCommand,
uploadedFile.SetImageDimensions(dimensions.Value.Width, dimensions.Value.Height);
}
var thumbnail = await _thumbnailService.GenerateImageThumbnailAsync(uploadResult.StoragePath);
var thumbnail = await _thumbnailService
.GenerateImageThumbnailAsync(uploadResult.StoragePath, category: "TaskChatMessage");
if (thumbnail.HasValue)
{
uploadedFile.SetThumbnail(thumbnail.Value.ThumbnailUrl);

View File

@@ -43,16 +43,37 @@ public class GetMessagesQueryHandler : IBaseQueryHandler<GetMessagesQuery, Pagin
.Take(request.PageSize)
.ToListAsync(cancellationToken);
// ✅ گرفتن تمامی کاربران برای نمایش نام کامل فرستنده به جای "کاربر"
// این بخش تمام UserId هایی که در پیام‌ها استفاده شده را جمع‌آوری می‌کند
// و یک Dictionary ایجاد می‌کند که UserId را به FullName نگاشت می‌کند
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) برای نمایش به صورت نوت
// در اینجا تمامی TaskSections مربوط به این تسک را می‌گیریم
// و برای هر کدام تمام AdditionalTimes آن را بارگذاری می‌کنیم
var taskSections = await _context.TaskSections
.Where(ts => ts.TaskId == request.TaskId)
.Include(ts => ts.AdditionalTimes)
.ToListAsync(cancellationToken);
var messageDtos = new List<MessageDto>();
foreach (var message in messages)
{
// ✅ نام فرستنده را از Dictionary Users بگیر، در صورت عدم وجود "کاربر ناشناس" نمایش بده
var senderName = users.ContainsKey(message.SenderUserId)
? users[message.SenderUserId]
: "کاربر ناشناس";
var dto = new MessageDto
{
Id = message.Id,
TaskId = message.TaskId,
SenderUserId = message.SenderUserId,
SenderName = "کاربر", // TODO: از User service
SenderName = senderName, // از User واقعی استفاده می‌کنیم
MessageType = message.MessageType.ToString(),
TextContent = message.TextContent,
ReplyToMessageId = message.ReplyToMessageId,
@@ -67,11 +88,16 @@ public class GetMessagesQueryHandler : IBaseQueryHandler<GetMessagesQuery, Pagin
if (message.ReplyToMessage != null)
{
// ✅ برای پیام‌های Reply نیز نام فرستنده را درست نمایش بده
var replySenderName = users.ContainsKey(message.ReplyToMessage.SenderUserId)
? users[message.ReplyToMessage.SenderUserId]
: "کاربر ناشناس";
dto.ReplyToMessage = new MessageDto
{
Id = message.ReplyToMessage.Id,
SenderUserId = message.ReplyToMessage.SenderUserId,
SenderName = "کاربر",
SenderName = replySenderName,
TextContent = message.ReplyToMessage.TextContent,
CreationDate = message.ReplyToMessage.CreationDate
};
@@ -98,8 +124,58 @@ public class GetMessagesQueryHandler : IBaseQueryHandler<GetMessagesQuery, Pagin
}
messageDtos.Add(dto);
// ✅ اینجا بخش جدید است: نوت‌های زمان اضافی را بین پیام‌ها اضافه کن
// این بخش تمام AdditionalTimes را که بعد از این پیام اضافه شده‌اند را پیدا می‌کند
var additionalTimesAfterMessage = taskSections
.SelectMany(ts => ts.AdditionalTimes)
.Where(at => at.AddedAt > message.CreationDate) // ✅ تغییر به AddedAt (زمان واقعی اضافه شدن)
.OrderBy(at => at.AddedAt)
.FirstOrDefault();
if (additionalTimesAfterMessage != null)
{
// ✅ تمام AdditionalTimes بین این پیام و پیام قبلی را بگیر
var additionalTimesByDate = taskSections
.SelectMany(ts => ts.AdditionalTimes)
.Where(at => at.AddedAt <= message.CreationDate &&
(messageDtos.Count == 1 || at.AddedAt > messageDtos[messageDtos.Count - 2].CreationDate))
.OrderBy(at => at.AddedAt)
.ToList();
foreach (var additionalTime in additionalTimesByDate)
{
// ✅ نام کاربری که این زمان اضافی را اضافه کرد
var addedByUserName = additionalTime.AddedByUserId.HasValue && users.TryGetValue(additionalTime.AddedByUserId.Value, out var user)
? user
: "سیستم";
// ✅ محتوای نوت را با اطلاعات کامل ایجاد کن
// نمایش می‌دهد: مقدار زمان + علت + نام کسی که اضافه کرد
var noteContent = $"⏱️ زمان اضافی: {additionalTime.Hours.TotalHours:F2} ساعت - {(string.IsNullOrWhiteSpace(additionalTime.Reason) ? "بدون علت" : additionalTime.Reason)} - توسط {addedByUserName}";
// ✅ نوت را به عنوان MessageDto خاصی ایجاد کن
var noteDto = new MessageDto
{
Id = Guid.NewGuid(),
TaskId = request.TaskId,
SenderUserId = 0, // ✅ سیستم برای نشان دادن اینکه یک پیام خودکار است
SenderName = "سیستم",
MessageType = "Note", // ✅ نوع پیام: Note (یادداشت سیستم)
TextContent = noteContent,
CreationDate = additionalTime.AddedAt, // ✅ تاریخ اضافه شدن زمان اضافی
IsMine = false
};
messageDtos.Add(noteDto);
}
}
}
// ✅ مرتب کردن نهایی تمام پیام‌ها (معمولی + نوت‌ها) بر اساس زمان ایجاد
// اینطور که نوت‌های زمان اضافی در جای درست خود قرار می‌گیرند
messageDtos = messageDtos.OrderBy(m => m.CreationDate).ToList();
var response = new PaginationResult<MessageDto>()
{
List = messageDtos,

View File

@@ -29,16 +29,25 @@ public class GetPinnedMessagesQueryHandler : IBaseQueryHandler<GetPinnedMessages
.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 = senderName,
MessageType = message.MessageType.ToString(),
TextContent = message.TextContent,
IsPinned = message.IsPinned,

View File

@@ -40,15 +40,24 @@ public class SearchMessagesQueryHandler : IBaseQueryHandler<SearchMessagesQuery,
.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 = senderName,
MessageType = message.MessageType.ToString(),
TextContent = message.TextContent,
CreationDate = message.CreationDate,

View File

@@ -9,15 +9,17 @@ public interface IThumbnailGeneratorService
/// تولید thumbnail برای تصویر
/// </summary>
Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync(
string imagePath,
int width = 200,
string imagePath,
string category,
int width = 200,
int height = 200);
/// <summary>
/// تولید thumbnail برای ویدیو
/// </summary>
Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(
string videoPath);
string videoPath,
string category);
/// <summary>
/// حذف thumbnail

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

@@ -62,17 +62,25 @@ public class TaskChatMessage : EntityBase<Guid>
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)
@@ -98,7 +106,7 @@ public class TaskChatMessage : EntityBase<Guid>
throw new BadRequestException("فقط فرستنده می‌تواند پیام را ویرایش کند");
}
if (MessageType != MessageType.Text)
if (MessageType != MessageType.Text || (MessageType != MessageType.Text && !string.IsNullOrWhiteSpace(TextContent)))
{
throw new BadRequestException("فقط پیام‌های متنی قابل ویرایش هستند");
}

View File

@@ -10,5 +10,6 @@ public enum MessageType
Image = 3, // تصویر
Voice = 4, // پیام صوتی
Video = 5, // ویدیو
Note = 6, // ✅ یادداشت سیستم (برای زمان اضافی و اطلاعات خودکار)
}

View File

@@ -24,11 +24,11 @@ public class LocalFileStorageService : IFileStorageService
var request = httpContextAccessor.HttpContext?.Request;
if (request != null)
{
_baseUrl = $"{request.Scheme}://{request.Host}/uploads";
_baseUrl = $"{request.Scheme}://{request.Host}/storage";
}
else
{
_baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/uploads";
_baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/storage";
}
// ایجاد پوشه اگر وجود نداشت

View File

@@ -1,4 +1,5 @@
using GozareshgirProgramManager.Application.Services.FileManagement;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
@@ -14,12 +15,21 @@ public class ThumbnailGeneratorService : IThumbnailGeneratorService
private readonly string _thumbnailBasePath;
private readonly string _baseUrl;
public ThumbnailGeneratorService(IConfiguration configuration)
public ThumbnailGeneratorService(IConfiguration configuration,
IHttpContextAccessor httpContextAccessor)
{
_thumbnailBasePath = configuration["FileStorage:ThumbnailPath"]
?? Path.Combine(Directory.GetCurrentDirectory(), "Uploads", "Thumbnails");
?? Path.Combine(Directory.GetCurrentDirectory(), "storage", "Thumbnails");
var request = httpContextAccessor.HttpContext?.Request;
_baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/uploads";
if (request != null)
{
_baseUrl = $"{request.Scheme}://{request.Host}/storage";
}
else
{
_baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/storage";
}
if (!Directory.Exists(_thumbnailBasePath))
{
@@ -28,7 +38,8 @@ public class ThumbnailGeneratorService : IThumbnailGeneratorService
}
public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync(
string imagePath,
string imagePath,
string category,
int width = 200,
int height = 200)
{
@@ -54,14 +65,12 @@ public class ThumbnailGeneratorService : IThumbnailGeneratorService
var extension = Path.GetExtension(imagePath);
var thumbnailFileName = $"{fileName}_thumb{extension}";
// مسیر ذخیره thumbnail
var thumbnailPath = Path.Combine(_thumbnailBasePath, thumbnailFileName);
// دریافت مسیر و URL توسط متد private
var (thumbnailPath, thumbnailUrl) = GetThumbnailPathAndUrl(thumbnailFileName, category);
// ذخیره thumbnail با کیفیت 80
await image.SaveAsync(thumbnailPath, new JpegEncoder { Quality = 80 });
// URL thumbnail
var thumbnailUrl = $"{_baseUrl}/thumbnails/{thumbnailFileName}";
return (thumbnailPath, thumbnailUrl);
}
@@ -72,7 +81,7 @@ public class ThumbnailGeneratorService : IThumbnailGeneratorService
}
}
public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(string videoPath)
public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(string videoPath, string category = "general")
{
// TODO: برای Video thumbnail باید از FFmpeg استفاده کنیم
// فعلاً یک placeholder image برمی‌گردانیم
@@ -107,5 +116,29 @@ public class ThumbnailGeneratorService : IThumbnailGeneratorService
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);
}
}

View File

@@ -165,4 +165,12 @@ public class ProjectController : ProgramManagerBaseController
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;
}
}

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"),
@@ -493,7 +487,7 @@ app.UseHttpsRedirection();
app.UseStaticFiles();
// Static files برای فایل‌های آپلود شده
var uploadsPath = builder.Configuration["FileStorage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
var uploadsPath = builder.Configuration["FileStorage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "Storage");
if (!Directory.Exists(uploadsPath))
{
Directory.CreateDirectory(uploadsPath);
@@ -502,7 +496,7 @@ if (!Directory.Exists(uploadsPath))
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(uploadsPath),
RequestPath = "/uploads",
RequestPath = "/storage",
OnPrepareResponse = ctx =>
{
// Cache برای فایل‌ها (30 روز)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB