Compare commits

..

47 Commits

Author SHA1 Message Date
73e6681baa add message type to search query 2026-01-14 10:46:44 +03:30
80a58f8cdc feat: add TaskId to ProjectBoardListQueryHandler and ProjectBoardListResponse for enhanced task tracking 2026-01-08 12:14:26 +03:30
c8dddabdff fix: correct logic for editing text messages in TaskChatMessage.cs 2026-01-08 11:05:36 +03:30
95d66c2d89 feat: enhance message queries to display real sender names and add system notes for additional times 2026-01-07 11:17:16 +03:30
609daf4353 feat: update file storage paths and enhance thumbnail generation with category support 2026-01-07 10:50:23 +03:30
a81e01ce2b Remove Storage folder from git tracking 2026-01-07 10:44:09 +03:30
2cd838a5e3 feat: enhance thumbnail generation with category support and update storage paths 2026-01-07 10:25:33 +03:30
34bd7ba444 add set file message to TaskChatMessage.cs 2026-01-06 12:21:12 +03:30
43b124664e feat: integrate authentication checks in message command handlers 2026-01-05 16:06:35 +03:30
d2dd67343b feat: add file management entities and services for chat message handling 2026-01-05 15:40:06 +03:30
3d2b5ff6bd feat: implement TaskChatMessage entity and repository for chat message management 2026-01-05 11:45:37 +03:30
209aa5912d feat: configure Serilog for file logging with environment-specific settings 2026-01-05 10:58:30 +03:30
340685a06c feat: enhance project retrieval by including additional times and update search parameter naming 2026-01-05 09:49:31 +03:30
b20a56df26 Merge branch 'Feature/program-manager/set-user-time' 2026-01-04 20:39:49 +03:30
8d93fa4fc6 Add approval workflow for task section completion
- Updated status transitions to include PendingForCompletion before Completed
- Added API endpoint and command/handler for approving/rejecting section completion
- Fixed additional time calculation to include minutes
- Refactored project board query logic and improved user ordering
- Updated launch settings with new HTTPS endpoint
- Documented progress percentage feature for TaskSection
2026-01-04 20:39:20 +03:30
8f10f7057c fix: handle null workshop details in ticket display logic 2026-01-04 17:54:59 +03:30
00b5066f6f feat: implement removal of invalid project, phase, and task sections based on skill validation 2026-01-04 17:40:42 +03:30
abd221cb55 feat: fix SkillName and SkillId assignment in ProjectSetTimeDetailsQueryHandler 2026-01-04 17:18:36 +03:30
33833a408c feat: include PhaseSections in ProjectPhase retrieval for enhanced task data 2026-01-04 16:58:36 +03:30
c2fca9f9eb feat: rename project hierarchy search components and add validation for search query 2026-01-04 15:50:56 +03:30
a16c20440b fix: correct payment amount calculation in installment function 2026-01-04 15:32:56 +03:30
1f365f3642 feat: implement project hierarchy search functionality with validation 2026-01-04 15:14:28 +03:30
0bfcde6a3f feat: add validation to prevent users from starting multiple sections in progress 2026-01-04 14:13:29 +03:30
4ada29a98a feat: enhance ProjectSetTimeDetailsQuery to support multiple hierarchy levels and improve data retrieval 2026-01-04 13:51:36 +03:30
6f64ee1ce4 refactor: streamline ProjectSetTimeDetailsQueryHandler to enhance skill and user data retrieval 2026-01-04 13:01:43 +03:30
3340edcc17 feat: add total hours and minutes tracking to project, phase, and task calculations 2026-01-01 15:41:58 +03:30
385a885c93 feat: add TotalHours and Minutes to project and task details for improved time tracking 2026-01-01 14:13:27 +03:30
f99f199a77 refactor: rename SectionItems to SkillItems and add CreationDate to project time details 2026-01-01 13:16:52 +03:30
287b31e356 refactor: improve code readability by formatting and organizing constructor parameters 2026-01-01 12:50:34 +03:30
5f8232809a refactor: update project time management to use skills and improve data structure 2026-01-01 12:29:06 +03:30
3c72311096 refactor: update namespace for AuthorizedBankDetails application components 2026-01-01 10:11:17 +03:30
250d17eba2 Merge remote-tracking branch 'origin/master' 2025-12-31 20:22:53 +03:30
5db8e7d319 show initial workshops on print instead of current 2025-12-31 20:22:42 +03:30
SamSys
3300f60845 change 2025-12-31 20:08:11 +03:30
SamSys
7537cfe5b8 Merge branch 'master' of https://github.com/samsyntax24/OriginalGozareshgir 2025-12-31 20:07:35 +03:30
SamSys
3c1bf7dff0 Log download 2025-12-31 20:07:18 +03:30
6909fcf715 remove financial transaction on institutioncontract veerification 2025-12-31 20:06:56 +03:30
fe66ff5aa3 add: log response content in SepehrPaymentGateway 2025-12-31 19:26:12 +03:30
ce305edac4 add: integrate ILogger for SepehrPaymentGateway logging 2025-12-31 19:12:12 +03:30
a49b825ce9 Merge remote-tracking branch 'origin/master' 2025-12-31 19:07:09 +03:30
fb62523a23 add: integrate Serilog for enhanced logging and monitoring 2025-12-31 19:06:56 +03:30
SamSys
e171a4749c change 2025-12-31 18:34:42 +03:30
9b6c0d4cc4 Merge remote-tracking branch 'origin/master' 2025-12-31 18:31:12 +03:30
f8126b4000 set transaction id on get 2025-12-31 18:31:00 +03:30
SamSys
cf62d75f0e change 2025-12-31 18:23:07 +03:30
SamSys
f93e59b77c Merge branch 'master' of https://github.com/samsyntax24/OriginalGozareshgir 2025-12-31 18:21:15 +03:30
SamSys
8f37d9f388 init SendWarningSms and LegalActionSms 2025-12-31 18:21:01 +03:30
143 changed files with 5413 additions and 298 deletions

4
.gitignore vendored
View File

@@ -362,3 +362,7 @@ MigrationBackup/
# # Fody - auto-generated XML schema
# FodyWeavers.xsd
.idea
# Storage folder - ignore all uploaded files, thumbnails, and temporary files
ServiceHost/Storage

View File

@@ -4,6 +4,7 @@ using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace _0_Framework.Application.PaymentGateway;
@@ -12,18 +13,24 @@ public class SepehrPaymentGateway:IPaymentGateway
{
private readonly HttpClient _httpClient;
private const long TerminalId = 99213700;
private readonly ILogger<SepehrPaymentGateway> _logger;
public SepehrPaymentGateway(IHttpClientFactory httpClient)
public SepehrPaymentGateway(IHttpClientFactory httpClient, ILogger<SepehrPaymentGateway> logger)
{
_logger = logger;
_httpClient = httpClient.CreateClient();
_httpClient.BaseAddress = new Uri("https://sepehr.shaparak.ir/Rest/V1/PeymentApi/");
}
public async Task<PaymentGatewayResponse> Create(CreatePaymentGatewayRequest command, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Create payment started. TransactionId: {TransactionId}, Amount: {Amount}", command.TransactionId, command.Amount);
command.ExtraData ??= new Dictionary<string, object>();
_logger.LogInformation("Initializing extra data with FinancialInvoiceId: {FinancialInvoiceId}", command.FinancialInvoiceId);
command.ExtraData.Add("financialInvoiceId", command.FinancialInvoiceId);
var extraData = JsonConvert.SerializeObject(command.ExtraData);
_logger.LogInformation("Serialized extra data payload: {Payload}", extraData);
var res = await _httpClient.PostAsJsonAsync("GetToken", new
{
TerminalID = TerminalId,
@@ -32,21 +39,25 @@ public class SepehrPaymentGateway:IPaymentGateway
callbackURL = command.CallBackUrl,
payload = extraData
}, cancellationToken: cancellationToken);
_logger.LogInformation("Create payment request sent. StatusCode: {StatusCode}", res.StatusCode);
// خواندن محتوای پاسخ
var content = await res.Content.ReadAsStringAsync(cancellationToken);
_logger.LogInformation("Create payment response content: {Content}", content);
// تبدیل پاسخ JSON به آبجکت دات‌نت
var json = System.Text.Json.JsonDocument.Parse(content);
_logger.LogInformation("Create payment JSON parsed successfully.");
// گرفتن مقدار AccessToken
var accessToken = json.RootElement.GetProperty("Accesstoken").ToString();
var status = json.RootElement.GetProperty("Status").ToString();
_logger.LogInformation("Create payment parsed values. Status: {Status}, AccessToken: {AccessToken}", status, accessToken);
return new PaymentGatewayResponse
{
Status = status,
IsSuccess = status == "0",
Token = accessToken
Token = accessToken,
};
}
@@ -55,21 +66,24 @@ public class SepehrPaymentGateway:IPaymentGateway
public async Task<PaymentGatewayResponse> Verify(VerifyPaymentGateWayRequest command, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Verify payment started. DigitalReceipt: {DigitalReceipt}", command.DigitalReceipt);
var res = await _httpClient.PostAsJsonAsync("Advice", new
{
digitalreceipt = command.DigitalReceipt,
Tid = TerminalId,
}, cancellationToken: cancellationToken);
_logger.LogInformation("Verify payment request sent. StatusCode: {StatusCode}", res.StatusCode);
// خواندن محتوای پاسخ
var content = await res.Content.ReadAsStringAsync(cancellationToken);
_logger.LogInformation("Verify payment response content: {Content}", content);
// تبدیل پاسخ JSON به آبجکت دات‌نت
var json = System.Text.Json.JsonDocument.Parse(content);
_logger.LogInformation("Verify payment JSON parsed successfully.");
var message = json.RootElement.GetProperty("Message").GetString();
var status = json.RootElement.GetProperty("Status").GetString();
_logger.LogInformation("Verify payment parsed values. Status: {Status}, Message: {Message}", status, message);
return new PaymentGatewayResponse
{
Status = status,

View File

@@ -18,7 +18,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Shared.Contracts.PmUser.Commands;
using Shared.Contracts.PmUser.Queries;
@@ -48,7 +47,13 @@ public class AccountApplication : IAccountApplication
private readonly IPmUserCommandService _pmUserCommandService;
public AccountApplication(IAccountRepository accountRepository, IPasswordHasher passwordHasher,
IFileUploader fileUploader, IAuthHelper authHelper, IRoleRepository roleRepository, IWorker worker, ISmsService smsService, ICameraAccountRepository cameraAccountRepository, IPositionRepository positionRepository, IAccountLeftworkRepository accountLeftworkRepository, IWorkshopRepository workshopRepository, ISubAccountRepository subAccountRepository, ISubAccountRoleRepository subAccountRoleRepository, IWorkshopSubAccountRepository workshopSubAccountRepository, ISubAccountPermissionSubtitle1Repository accountPermissionSubtitle1Repository, IUnitOfWork unitOfWork, IPmUserQueryService pmUserQueryService, IPmUserCommandService pmUserCommandService)
IFileUploader fileUploader, IAuthHelper authHelper, IRoleRepository roleRepository, IWorker worker,
ISmsService smsService, ICameraAccountRepository cameraAccountRepository,
IPositionRepository positionRepository, IAccountLeftworkRepository accountLeftworkRepository,
IWorkshopRepository workshopRepository, ISubAccountRepository subAccountRepository,
ISubAccountRoleRepository subAccountRoleRepository, IWorkshopSubAccountRepository workshopSubAccountRepository,
ISubAccountPermissionSubtitle1Repository accountPermissionSubtitle1Repository, IUnitOfWork unitOfWork,
IPmUserQueryService pmUserQueryService, IPmUserCommandService pmUserCommandService)
{
_authHelper = authHelper;
_roleRepository = roleRepository;
@@ -68,7 +73,6 @@ public class AccountApplication : IAccountApplication
_fileUploader = fileUploader;
_passwordHasher = passwordHasher;
_accountRepository = accountRepository;
}
public OperationResult EditClient(EditClientAccount command)
@@ -89,7 +93,8 @@ public class AccountApplication : IAccountApplication
(x.Mobile == command.Mobile && x.id != command.Id)))
return opreation.Failed("شماره موبایل تکراری است");
if (_accountRepository.Exists(x =>
(x.NationalCode == command.NationalCode && !string.IsNullOrWhiteSpace(x.NationalCode) && x.id != command.Id)))
(x.NationalCode == command.NationalCode && !string.IsNullOrWhiteSpace(x.NationalCode) &&
x.id != command.Id)))
return opreation.Failed("کد ملی تکراری است");
if (_accountRepository.Exists(x =>
(x.Email == command.Email && !string.IsNullOrWhiteSpace(x.Email) && x.id != command.Id)))
@@ -97,7 +102,8 @@ public class AccountApplication : IAccountApplication
var path = $"profilePhotos";
var picturePath = _fileUploader.Upload(command.ProfilePhoto, path);
editAccount.EditClient(command.Fullname, command.Username, command.Mobile, picturePath, command.Email, command.NationalCode);
editAccount.EditClient(command.Fullname, command.Username, command.Mobile, picturePath, command.Email,
command.NationalCode);
_accountRepository.SaveChanges();
return opreation.Succcedded();
}
@@ -142,8 +148,8 @@ public class AccountApplication : IAccountApplication
if (_fileUploader != null)
{
picturePath = _fileUploader.Upload(command.ProfilePhoto, path);
}
var account = new Account(command.Fullname, command.Username, password, command.Mobile, command.RoleId,
picturePath, roleName.Name, "true", "false");
@@ -158,7 +164,8 @@ public class AccountApplication : IAccountApplication
if (command.UserRoles == null)
return operation.Failed("حداقل یک نقش برای کاربر مدیریت پروژه لازم است");
var pmUserRoles = command.UserRoles.Where(x => x > 0).ToList();
var createPm = await _pmUserCommandService.Create(new CreatePmUserDto(command.Fullname, command.Username, account.Password, command.Mobile,
var createPm = await _pmUserCommandService.Create(new CreatePmUserDto(command.Fullname, command.Username,
account.Password, command.Mobile,
null, account.id, pmUserRoles));
if (!createPm.isSuccess)
{
@@ -167,7 +174,6 @@ public class AccountApplication : IAccountApplication
}
//var url = "api/user/create";
//var key = SecretKeys.ProgramManagerInternalApi;
@@ -252,29 +258,30 @@ public class AccountApplication : IAccountApplication
// $"api/user/{account.id}",
// key
//);
var userResult =await _pmUserQueryService.GetPmUserDataByAccountId(account.id);
var userResult = await _pmUserQueryService.GetPmUserDataByAccountId(account.id);
if (command.UserRoles == null)
return operation.Failed("حداقل یک نقش برای کاربر مدیریت پروژه لازم است");
var pmUserRoles = command.UserRoles.Where(x => x > 0).ToList();
//اگر کاربر در پروگرام منیجر قبلا ایجاد شده
if (userResult.Id >0)
if (userResult.Id > 0)
{
if (!command.UserRoles.Any())
{
_unitOfWork.RollbackAccountContext();
return operation.Failed("حداقل یک نقش باید انتخاب شود");
}
var editPm =await _pmUserCommandService.Edit(new EditPmUserDto(command.Fullname, command.Username, command.Mobile, account.id, pmUserRoles,
var editPm = await _pmUserCommandService.Edit(new EditPmUserDto(command.Fullname, command.Username,
command.Mobile, account.id, pmUserRoles,
command.IsProgramManagerUser));
if (!editPm.isSuccess)
{
_unitOfWork.RollbackAccountContext();
return operation.Failed("خطا در ویرایش کاربر پروگرام منیجر");
}
//var parameters = new EditUserCommand(
// command.Fullname,
// command.Username,
@@ -302,7 +309,6 @@ public class AccountApplication : IAccountApplication
// _unitOfWork.RollbackAccountContext();
// return operation.Failed(response.Error);
//}
}
else //اگر کاربر قبلا ایجاد نشده
{
@@ -315,19 +321,15 @@ public class AccountApplication : IAccountApplication
return operation.Failed("حداقل یک نقش باید انتخاب شود");
}
var createPm = await _pmUserCommandService.Create(new CreatePmUserDto(command.Fullname, command.Username, account.Password, command.Mobile,
var createPm = await _pmUserCommandService.Create(new CreatePmUserDto(command.Fullname,
command.Username, account.Password, command.Mobile,
null, account.id, pmUserRoles));
if (!createPm.isSuccess)
{
_unitOfWork.RollbackAccountContext();
return operation.Failed("خطا در ویرایش کاربر پروگرام منیجر");
}
//var parameters = new CreateProgramManagerUser(
@@ -362,7 +364,6 @@ public class AccountApplication : IAccountApplication
// return operation.Failed(response.Error);
//}
}
}
_unitOfWork.CommitAccountContext();
@@ -376,7 +377,6 @@ public class AccountApplication : IAccountApplication
public OperationResult Login(Login command)
{
long idAutoriz = 0;
var operation = new OperationResult();
if (string.IsNullOrWhiteSpace(command.Password))
@@ -401,21 +401,27 @@ public class AccountApplication : IAccountApplication
.Select(x => x.Code)
.ToList();
//PmPermission
var PmUserData = _pmUserQueryService.GetPmUserDataByAccountId(account.id).GetAwaiter().GetResult();
if (PmUserData.AccountId > 0 && PmUserData.IsActive)
var PmUserData = _pmUserQueryService.GetPmUserDataByAccountId(account.id)
.GetAwaiter().GetResult();
long? pmUserId = null;
if (PmUserData != null)
{
var pmUserPermissions =
PmUserData.RoleListDto != null
? PmUserData.RoleListDto
.SelectMany(x => x.Permissions)
.Where(p => p != 99)
.Distinct()
.ToList()
: new List<int>();
permissions.AddRange(pmUserPermissions);
if (PmUserData.AccountId > 0 && PmUserData.IsActive)
{
var pmUserPermissions =
PmUserData.RoleListDto != null
? PmUserData.RoleListDto
.SelectMany(x => x.Permissions)
.Where(p => p != 99)
.Distinct()
.ToList()
: new List<int>();
permissions.AddRange(pmUserPermissions);
}
pmUserId = PmUserData.Id > 0 ? PmUserData.Id : null;
}
int? positionValue;
if (account.PositionId != null)
@@ -426,24 +432,25 @@ public class AccountApplication : IAccountApplication
{
positionValue = null;
}
var pmUserId = PmUserData.AccountId > 0 ? PmUserData.AccountId : null;
var authViewModel = new AuthViewModel(account.id, account.RoleId, account.Fullname
, account.Username, account.Mobile, account.ProfilePhoto,
permissions, account.RoleName, account.AdminAreaPermission,
account.ClientAriaPermission, positionValue,0,pmUserId);
permissions, account.RoleName, account.AdminAreaPermission,
account.ClientAriaPermission, positionValue, 0, pmUserId);
if (account.ClientAriaPermission == "true" && account.AdminAreaPermission == "false" &&
account.IsActiveString == "true")
{
var clientPermissions = _accountPermissionSubtitle1Repository.GetAllPermissionCodes();
authViewModel.Permissions = clientPermissions;
var workshopList = _workshopRepository.GetWorkshopsByClientAccountId(account.id).Select(x => new WorkshopClaim
{
PersonnelCount = x.PersonnelCount,
Id = x.Id,
Name = x.WorkshopFullName,
Slug = _passwordHasher.SlugHasher(x.Id)
}).OrderByDescending(x => x.PersonnelCount).ToList();
var workshopList = _workshopRepository.GetWorkshopsByClientAccountId(account.id).Select(x =>
new WorkshopClaim
{
PersonnelCount = x.PersonnelCount,
Id = x.Id,
Name = x.WorkshopFullName,
Slug = _passwordHasher.SlugHasher(x.Id)
}).OrderByDescending(x => x.PersonnelCount).ToList();
authViewModel.WorkshopList = workshopList;
if (workshopList.Any())
{
@@ -456,10 +463,14 @@ public class AccountApplication : IAccountApplication
_authHelper.Signin(authViewModel);
if ((account.AdminAreaPermission == "true" && account.ClientAriaPermission == "true" && account.IsActiveString == "true") || (account.AdminAreaPermission == "true" && account.ClientAriaPermission == "false" && account.IsActiveString == "true"))
if ((account.AdminAreaPermission == "true" && account.ClientAriaPermission == "true" &&
account.IsActiveString == "true") || (account.AdminAreaPermission == "true" &&
account.ClientAriaPermission == "false" &&
account.IsActiveString == "true"))
idAutoriz = 1;
if (account.ClientAriaPermission == "true" && account.AdminAreaPermission == "false" && account.IsActiveString == "true")
if (account.ClientAriaPermission == "true" && account.AdminAreaPermission == "false" &&
account.IsActiveString == "true")
idAutoriz = 2;
}
@@ -471,7 +482,8 @@ public class AccountApplication : IAccountApplication
var mobile = string.IsNullOrWhiteSpace(cameraAccount.Mobile) ? " " : cameraAccount.Mobile;
var authViewModel = new CameraAuthViewModel(cameraAccount.id, cameraAccount.WorkshopId,
cameraAccount.Username, mobile, cameraAccount.WorkshopName, cameraAccount.AccountId, cameraAccount.IsActiveSting);
cameraAccount.Username, mobile, cameraAccount.WorkshopName, cameraAccount.AccountId,
cameraAccount.IsActiveSting);
if (cameraAccount.IsActiveSting == "true")
{
_authHelper.CameraSignIn(authViewModel);
@@ -481,7 +493,6 @@ public class AccountApplication : IAccountApplication
{
idAutoriz = 0;
}
}
if (subAccount != null)
@@ -511,12 +522,14 @@ public class AccountApplication : IAccountApplication
authViewModel.WorkshopSlug = _passwordHasher.SlugHasher(workshop.WorkshopId);
authViewModel.WorkshopId = workshop.WorkshopId;
}
_authHelper.Signin(authViewModel);
idAutoriz = 2;
}
return operation.Succcedded(idAutoriz);
}
public OperationResult LoginWithMobile(long id)
{
var operation = new OperationResult();
@@ -525,7 +538,6 @@ public class AccountApplication : IAccountApplication
return operation.Failed(ApplicationMessages.WrongUserPass);
var permissions = _roleRepository.Get(account.RoleId)
.Permissions
.Select(x => x.Code)
@@ -541,20 +553,22 @@ public class AccountApplication : IAccountApplication
}
var authViewModel = new AuthViewModel(account.id, account.RoleId, account.Fullname
, account.Username, account.Mobile, account.ProfilePhoto, permissions, account.RoleName, account.AdminAreaPermission, account.ClientAriaPermission, positionValue);
, account.Username, account.Mobile, account.ProfilePhoto, permissions, account.RoleName,
account.AdminAreaPermission, account.ClientAriaPermission, positionValue);
if (account.ClientAriaPermission == "true" && account.AdminAreaPermission == "false" &&
account.IsActiveString == "true")
{
var clientPermissions = _accountPermissionSubtitle1Repository.GetAllPermissionCodes();
authViewModel.Permissions = clientPermissions;
var workshopList = _workshopRepository.GetWorkshopsByClientAccountId(account.id).Select(x => new WorkshopClaim
{
PersonnelCount = x.PersonnelCount,
Id = x.Id,
Name = x.WorkshopFullName,
Slug = _passwordHasher.SlugHasher(x.Id)
}).OrderByDescending(x => x.PersonnelCount).ToList();
var workshopList = _workshopRepository.GetWorkshopsByClientAccountId(account.id).Select(x =>
new WorkshopClaim
{
PersonnelCount = x.PersonnelCount,
Id = x.Id,
Name = x.WorkshopFullName,
Slug = _passwordHasher.SlugHasher(x.Id)
}).OrderByDescending(x => x.PersonnelCount).ToList();
authViewModel.WorkshopList = workshopList;
if (workshopList.Any())
{
@@ -567,13 +581,15 @@ public class AccountApplication : IAccountApplication
_authHelper.Signin(authViewModel);
long idAutoriz = 0;
if (account.AdminAreaPermission == "true" && account.ClientAriaPermission == "true" || account.AdminAreaPermission == "true" && account.ClientAriaPermission == "false")
if (account.AdminAreaPermission == "true" && account.ClientAriaPermission == "true" ||
account.AdminAreaPermission == "true" && account.ClientAriaPermission == "false")
idAutoriz = 1;
if (account.ClientAriaPermission == "true" && account.AdminAreaPermission == "false")
idAutoriz = 2;
return operation.Succcedded(idAutoriz);
}
public void Logout()
{
_authHelper.SignOut();
@@ -609,6 +625,7 @@ public class AccountApplication : IAccountApplication
_accountRepository.SaveChanges();
return operation.Succcedded();
}
public EditAccount GetByVerifyCode(string code, string phone)
{
return _accountRepository.GetByVerifyCode(code, phone);
@@ -637,7 +654,6 @@ public class AccountApplication : IAccountApplication
await _accountRepository.RemoveCode(id);
return operation.Succcedded();
}
@@ -682,7 +698,6 @@ public class AccountApplication : IAccountApplication
return operation.Failed("این اکانت وجود ندارد");
var permissions = _roleRepository.Get(account.RoleId)
.Permissions
.Select(x => x.Code)
@@ -691,7 +706,8 @@ public class AccountApplication : IAccountApplication
_authHelper.SignOut();
var authViewModel = new AuthViewModel(account.id, account.RoleId, account.Fullname
, account.Username, account.Mobile, account.ProfilePhoto, permissions, account.RoleName, "false", "true", null);
, account.Username, account.Mobile, account.ProfilePhoto, permissions, account.RoleName, "false", "true",
null);
var workshopList = _workshopRepository.GetWorkshopsByClientAccountId(account.id).Select(x => new WorkshopClaim
{
PersonnelCount = x.PersonnelCount,
@@ -711,9 +727,11 @@ public class AccountApplication : IAccountApplication
authViewModel.WorkshopName = workshop.Name;
authViewModel.WorkshopId = workshop.Id;
}
_authHelper.Signin(authViewModel);
return operation.Succcedded(2);
}
public OperationResult DirectCameraLogin(long cameraAccountId)
{
var prAcc = _authHelper.CurrentAccountInfo();
@@ -723,24 +741,22 @@ public class AccountApplication : IAccountApplication
return operation.Failed("این اکانت وجود ندارد");
_authHelper.SignOut();
var mobile = string.IsNullOrWhiteSpace(cameraAccount.Mobile) ? " " : cameraAccount.Mobile;
var authViewModel = new CameraAuthViewModel(cameraAccount.id, cameraAccount.WorkshopId,
cameraAccount.Username, mobile, cameraAccount.WorkshopName, cameraAccount.AccountId, cameraAccount.IsActiveSting);
cameraAccount.Username, mobile, cameraAccount.WorkshopName, cameraAccount.AccountId,
cameraAccount.IsActiveSting);
if (cameraAccount.IsActiveSting == "true")
{
_authHelper.CameraSignIn(authViewModel);
}
else
{
return operation.Failed("این اکانت غیر فعال شده است");
}
return operation.Succcedded(2);
}
@@ -773,10 +789,12 @@ public class AccountApplication : IAccountApplication
{
return this._accountLeftworkRepository.SaveWorkshopAccount(workshopAccountList, startDate, leftDate, accountId);
}
public OperationResult CreateNewWorkshopAccount(long currentAccountId, long newAccountId)
{
return this._accountLeftworkRepository.CopyWorkshopToNewAccount(currentAccountId, newAccountId);
}
#region Mahan
public List<AccountViewModel> AccountsForAssign(long taskId)
@@ -790,6 +808,7 @@ public class AccountApplication : IAccountApplication
{
return new List<AccountViewModel>();
}
return _accountRepository.GetAccountsByPositionId(positionId);
}
@@ -807,7 +826,6 @@ public class AccountApplication : IAccountApplication
return operation.Failed("این اکانت وجود ندارد");
var permissions = _roleRepository.Get(account.RoleId)
.Permissions
.Select(x => x.Code)
@@ -816,10 +834,10 @@ public class AccountApplication : IAccountApplication
_authHelper.SignOut();
var authViewModel = new AuthViewModel(account.id, account.RoleId, account.Fullname
, account.Username, account.Mobile, account.ProfilePhoto, permissions, account.RoleName, account.AdminAreaPermission, account.ClientAriaPermission, account.Position.PositionValue);
, account.Username, account.Mobile, account.ProfilePhoto, permissions, account.RoleName,
account.AdminAreaPermission, account.ClientAriaPermission, account.Position.PositionValue);
_authHelper.Signin(authViewModel);
return operation.Succcedded(2);
}
public async Task<List<AccountSelectListViewModel>> GetAdminSelectList()
@@ -828,8 +846,11 @@ public class AccountApplication : IAccountApplication
}
#endregion
#region Pooya
public OperationResult IsPhoneNumberAndPasswordValid(long accountId, string phoneNumber, string password, string rePassword)
public OperationResult IsPhoneNumberAndPasswordValid(long accountId, string phoneNumber, string password,
string rePassword)
{
OperationResult op = new();
@@ -847,7 +868,8 @@ public class AccountApplication : IAccountApplication
return op.Failed("رمز عبور نمی تواند کمتر از 8 کاراکتر باشد");
}
if ((string.IsNullOrWhiteSpace(phoneNumber) || entity.Mobile == phoneNumber) && string.IsNullOrWhiteSpace(rePassword))
if ((string.IsNullOrWhiteSpace(phoneNumber) || entity.Mobile == phoneNumber) &&
string.IsNullOrWhiteSpace(rePassword))
return op.Failed("چیزی برای تغییر وجود ندارد");
@@ -873,20 +895,22 @@ public class AccountApplication : IAccountApplication
var entity = _accountRepository.Get(command.AccountId);
if (entity == null)
return op.Failed(ApplicationMessages.RecordNotFound);
var validationResult = IsPhoneNumberAndPasswordValid(command.AccountId, command.PhoneNumber, command.Password, command.RePassword);
var validationResult = IsPhoneNumberAndPasswordValid(command.AccountId, command.PhoneNumber, command.Password,
command.RePassword);
if (validationResult.IsSuccedded == false)
return validationResult;
if (!string.IsNullOrWhiteSpace(command.RePassword))
{
entity.ChangePassword(_passwordHasher.Hash(command.Password));
}
if (!string.IsNullOrWhiteSpace(command.PhoneNumber))
{
entity.Edit(entity.Fullname, entity.Username, command.PhoneNumber, entity.RoleId, entity.ProfilePhoto, entity.RoleName);
entity.Edit(entity.Fullname, entity.Username, command.PhoneNumber, entity.RoleId, entity.ProfilePhoto,
entity.RoleName);
}
_accountRepository.SaveChanges();
return op.Succcedded();
}
@@ -982,6 +1006,7 @@ public class AccountApplication : IAccountApplication
// return claimsResponse.Failed(ApplicationMessages.WrongUserPass);
//}
#endregion
@@ -1024,5 +1049,4 @@ public class AccountApplication : IAccountApplication
{
return await _pmUserQueryService.GetPmUserDataByAccountId(accountId);
}
}

View File

@@ -58,17 +58,17 @@ public class JobSchedulerRegistrator
"*/1 * * * *" // هر 1 دقیقه یکبار چک کن
);
RecurringJob.AddOrUpdate(
"InstitutionContract.SendWarningSms",
() => SendWarningSms(),
"*/1 * * * *" // هر 1 دقیقه یکبار چک کن
);
//RecurringJob.AddOrUpdate(
// "InstitutionContract.SendWarningSms",
// () => SendWarningSms(),
// "*/1 * * * *" // هر 1 دقیقه یکبار چک کن
//);
RecurringJob.AddOrUpdate(
"InstitutionContract.SendLegalActionSms",
() => SendLegalActionSms(),
"*/1 * * * *" // هر 1 دقیقه یکبار چک کن
);
//RecurringJob.AddOrUpdate(
// "InstitutionContract.SendLegalActionSms",
// () => SendLegalActionSms(),
// "*/1 * * * *" // هر 1 دقیقه یکبار چک کن
//);
}

View File

@@ -2,7 +2,7 @@ using System.Collections.Generic;
using _0_Framework.Application;
using Company.Application.Contracts.AuthorizedBankDetails;
namespace Company.Application.Contracts.AuthorizedBankDetails
namespace CompanyManagment.App.Contracts.AuthorizedBankDetails
{
public interface IAuthorizedBankDetailsApplication
{

View File

@@ -109,4 +109,14 @@ public class BlockSmsListData
/// پابلیک آی دی بخش دو
/// </summary>
public string Code2 { get; set; }
}
/// <summary>
/// لیست قراداد های آبی
/// جهت ارسال هشدار یا اقدام قضائی
/// </summary>
public class BlueWarningSmsData
{
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using _0_Framework.Application;
using Company.Application.Contracts.AuthorizedBankDetails;
using Company.Domain.AuthorizedBankDetailsAgg;
using CompanyManagment.App.Contracts.AuthorizedBankDetails;
namespace CompanyManagment.Application
{

View File

@@ -26,6 +26,7 @@ using CompanyManagment.App.Contracts.InstitutionContractContactinfo;
using CompanyManagment.App.Contracts.PaymentTransaction;
using CompanyManagment.App.Contracts.SepehrPaymentGateway;
using CompanyManagment.App.Contracts.Workshop;
using Microsoft.Extensions.Logging;
using PersianTools.Core;
using ConnectedPersonnelViewModel = CompanyManagment.App.Contracts.Workshop.ConnectedPersonnelViewModel;
using FinancialStatment = Company.Domain.FinancialStatmentAgg.FinancialStatment;
@@ -61,7 +62,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
IAccountApplication accountApplication, ISmsService smsService,
IFinancialInvoiceRepository financialInvoiceRepository, IHttpClientFactory httpClientFactory,
IPaymentTransactionRepository paymentTransactionRepository, IRollCallServiceRepository rollCallServiceRepository,
ISepehrPaymentGatewayService sepehrPaymentGatewayService)
ISepehrPaymentGatewayService sepehrPaymentGatewayService,ILogger<SepehrPaymentGateway> sepehrGatewayLogger)
{
_institutionContractRepository = institutionContractRepository;
_contractingPartyRepository = contractingPartyRepository;
@@ -78,7 +79,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
_paymentTransactionRepository = paymentTransactionRepository;
_rollCallServiceRepository = rollCallServiceRepository;
_sepehrPaymentGatewayService = sepehrPaymentGatewayService;
_paymentGateway = new SepehrPaymentGateway(httpClientFactory);
_paymentGateway = new SepehrPaymentGateway(httpClientFactory,sepehrGatewayLogger);
}
public OperationResult Create(CreateInstitutionContract command)
@@ -1284,9 +1285,6 @@ public class InstitutionContractApplication : IInstitutionContractApplication
financialInvoice = await _financialInvoiceRepository.GetUnPaidByEntityId(firstInstallment.Id, FinancialInvoiceItemType.BuyInstitutionContractInstallment);
if (financialInvoice == null)
{
var financialTransaction = new FinancialTransaction(0, today, today.ToFarsi(),
"قسط اول سرویس", "debt", "بابت خدمات", firstInstallmentAmount, 0, 0);
financialStatement.AddFinancialTransaction(financialTransaction);
invoiceAmount = firstInstallmentAmount;
invoiceItemDescription = $"پرداخت قسط اول قرارداد شماره {institutionContract.ContractNo}";
invoiceItemType = FinancialInvoiceItemType.BuyInstitutionContractInstallment;
@@ -1305,9 +1303,6 @@ public class InstitutionContractApplication : IInstitutionContractApplication
financialInvoice = await _financialInvoiceRepository.GetUnPaidByEntityId(institutionContract.id, FinancialInvoiceItemType.BuyInstitutionContract);
if (financialInvoice == null)
{
var financialTransaction = new FinancialTransaction(0, today, today.ToFarsi(),
"پرداخت کل سرویس", "debt", "بابت خدمات", institutionContract.TotalAmount, 0, 0);
financialStatement.AddFinancialTransaction(financialTransaction);
invoiceAmount = institutionContract.TotalAmount;
invoiceItemDescription = $"پرداخت کل قرارداد شماره {institutionContract.ContractNo}";
invoiceItemType = FinancialInvoiceItemType.BuyInstitutionContract;

View File

@@ -18,6 +18,7 @@ using Company.Domain.FinancialStatmentAgg;
using Company.Domain.FinancialTransactionAgg;
using Company.Domain.InstitutionContractAgg;
using CompanyManagment.EFCore.Migrations;
using Microsoft.Extensions.Logging;
namespace CompanyManagment.Application;
@@ -37,7 +38,8 @@ public class PaymentCallbackHandler : IPaymentCallbackHandler
IFinancialInvoiceApplication financialInvoiceApplication,
IInstitutionContractApplication institutionContractApplication,
IHttpClientFactory httpClientFactory, IInstitutionContractRepository institutionContractRepository,
IFinancialStatmentRepository financialStatmentRepository)
IFinancialStatmentRepository financialStatmentRepository,
ILogger<SepehrPaymentGateway> sepehrGatewayLogger)
{
_paymentTransactionApplication = paymentTransactionApplication;
_financialStatmentApplication = financialStatmentApplication;
@@ -45,7 +47,7 @@ public class PaymentCallbackHandler : IPaymentCallbackHandler
_institutionContractApplication = institutionContractApplication;
_institutionContractRepository = institutionContractRepository;
_financialStatmentRepository = financialStatmentRepository;
_paymentGateway = new SepehrPaymentGateway(httpClientFactory);
_paymentGateway = new SepehrPaymentGateway(httpClientFactory, sepehrGatewayLogger);
}
/// <summary>

View File

@@ -7,6 +7,7 @@ using _0_Framework.Application;
using _0_Framework.Application.PaymentGateway;
using CompanyManagment.App.Contracts.PaymentTransaction;
using CompanyManagment.App.Contracts.SepehrPaymentGateway;
using Microsoft.Extensions.Logging;
namespace CompanyManagment.Application;
@@ -20,9 +21,10 @@ public class SepehrPaymentGatewayService : ISepehrPaymentGatewayService
public SepehrPaymentGatewayService(
IPaymentTransactionApplication paymentTransactionApplication,
IHttpClientFactory httpClientFactory)
IHttpClientFactory httpClientFactory,
ILogger<SepehrPaymentGateway> sepehrGatewayLogger)
{
_paymentGateway = new SepehrPaymentGateway(httpClientFactory);
_paymentGateway = new SepehrPaymentGateway(httpClientFactory, sepehrGatewayLogger);
_paymentTransactionApplication = paymentTransactionApplication;
}

View File

@@ -407,6 +407,10 @@ public class WorkshopAppliction : IWorkshopApplication
public EditWorkshop GetDetails(long id)
{
var workshop = _workshopRepository.GetDetails(id);
if (workshop == null)
{
return null;
}
if (workshop.IsClassified)
{
workshop.CreatePlan = _workshopPlanApplication.GetWorkshopPlanByWorkshopId(id);

View File

@@ -210,6 +210,7 @@ public class FinancialStatmentRepository : RepositoryBase<long, FinancialStatmen
}
return new FinancialTransactionDetailViewModel()
{
Id = t.id,
DateTimeGr = t.TdateGr,
DateFa = t.TdateGr.ToFarsi(),
TimeFa = $"{t.TdateGr:HH:mm}",

View File

@@ -2002,7 +2002,7 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
DiscountedAmount = discountAmount.ToMoney(),
DiscountPercetage = request.DiscountPercentage,
Installments = InstitutionMonthlyInstallmentCaculation((int)request.Duration,
totalAmount, DateTime.Now.ToFarsi()),
paymentAmount, DateTime.Now.ToFarsi()),
OneMonthAmount = discountedOneMonthAmount.ToMoney(),
Obligation = totalAmount.ToMoney()
};
@@ -3351,7 +3351,7 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
VerifyCode = institution.VerifyCode,
VerifyDate = institution.VerifyCodeCreation.ToFarsi(),
VerifyTime = institution.VerifyCodeCreation.ToString("HH:mm:ss"),
Workshops = institution.WorkshopGroup.CurrentWorkshops
Workshops = institution.WorkshopGroup.InitialWorkshops
.Select(x => new GetInstitutionVerificationDetailsWorkshopsViewModel()
{
Name = x.WorkshopName,
@@ -3561,7 +3561,7 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
public async Task<bool> SendReminderSmsForBackgroundTask()
{
var now = DateTime.Now;
_logger.LogInformation("================>> SendReminderSmsForBackgroundTask job run");
// تبدیل تاریخ میلادی به شمسی
var persianNow = now.ToFarsi();
@@ -6333,6 +6333,269 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
}
}
/// <summary>
///دریافت لیست پیامک قرادا های آبی بدهکار
/// </summary>
/// <returns></returns>
//public async Task<List<SmsListData>> GetBlueSmsListData()
//{
// 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();
// //حذف قراداد هایی که یکجا پرداخت شده اند از لیست ایجاد سند ماهانه
// institutionContracts = institutionContracts.Except(paidInFull).ToList();
// var contractingPartyList = await _context.PersonalContractingParties
// .Where(x => institutionContracts.Select(ins => ins.ContractingPartyId).Contains(x.id)).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 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 oldInstitutionContract = institutionContracts.Where(x => x.IsInstallment == false).ToList();
// var electronicInstitutionContract = institutionContracts.Where(x => x.IsInstallment == true).ToList();
// foreach (var item in oldInstitutionContract)
// {
// try
// {
// var contractingParty = GetDetails(item.ContractingPartyId);
// bool hasLegalActionSentSms = legalActionSentSms.Any(x => x.InstitutionContractId == item.Id);
// if (!string.IsNullOrWhiteSpace(contractingParty.LName) && !hasLegalActionSentSms)
// {
// //Thread.Sleep(500);
// var partyName = contractingParty.LName;
// if (partyName.Length > 25) partyName = $"{partyName.Substring(0, 25)}";
// 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);
// var hasPhonNumber = phoneNumberList.Any(x => x.InstitutionContractId == item.Id);
// if (hasFinancialStatement && hasPhonNumber)
// {
// var phoneNumbers = phoneNumberList.Where(x => x.InstitutionContractId == item.Id)
// .Select(x => new CreateContactInfo
// {
// PhoneType = x.PhoneType,
// PhoneNumber = x.PhoneNumber,
// 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 id = $"{item.ContractingPartyId}";
// var aprove = $"{transactions.Id}";
// var balance = debtor - creditor;
// if (balance > 0) // اگر بدهکار بود
// {
// if (isLegal)
// {
// if (item.OfficialCompany == "Official") // حقوقی بدهکار رسمی
// {
// var balanceToMoney = balance.ToMoney();
// foreach (var number in phoneNumbers)
// {
// 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, 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)
// {
// 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();
// 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);
// }
// }
// }
// }
// 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);
// }
// }
// }
// }
// }
// }
// }
// }
// }
// }
// 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");
// }
// }
//}
#endregion

View File

@@ -10,6 +10,7 @@ using CompanyManagment.App.Contracts.AuthorizedPerson;
using _0_Framework.Application;
using _0_Framework.Application.UID;
using Company.Application.Contracts.AuthorizedBankDetails;
using CompanyManagment.App.Contracts.AuthorizedBankDetails;
namespace CompanyManagment.EFCore.Services;

View File

@@ -89,6 +89,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackgroundInstitutionContract.Task", "BackgroundInstitutionContract\BackgroundInstitutionContract.Task\BackgroundInstitutionContract.Task.csproj", "{F78FBB92-294B-88BA-168D-F0C578B0D7D6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProgramManager", "ProgramManager", "{67AFF7B6-4C4F-464C-A90D-9BDB644D83A9}"
ProjectSection(SolutionItems) = preProject
ProgramManager\appsettings.FileStorage.json = ProgramManager\appsettings.FileStorage.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{48F6F6A5-7340-42F8-9216-BEB7A4B7D5A1}"
EndProject

View File

@@ -236,6 +236,7 @@ using P_TextManager.Domin.TextManagerAgg;
using CompanyManagment.App.Contracts.PaymentCallback;
using CompanyManagment.App.Contracts.SepehrPaymentGateway;
using Company.Domain.CameraBugReportAgg;
using CompanyManagment.App.Contracts.AuthorizedBankDetails;
using CompanyManagment.App.Contracts.CameraBugReport;
using CameraBugReportRepository = CompanyManagement.Infrastructure.Mongo.CameraBugReportRepo.CameraBugReportRepository;
using CompanyManagment.EFCore.Services;

View File

@@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="MediatR" Version="14.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
</ItemGroup>
@@ -18,4 +19,10 @@
<ProjectReference Include="..\..\Domain\GozareshgirProgramManager.Domain\GozareshgirProgramManager.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http.Features">
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.AspNetCore.Http.Features.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,57 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion;
public record ApproveTaskSectionCompletionCommand(Guid TaskSectionId, bool IsApproved) : IBaseCommand;
public class ApproveTaskSectionCompletionCommandHandler : IBaseCommandHandler<ApproveTaskSectionCompletionCommand>
{
private readonly ITaskSectionRepository _taskSectionRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IAuthHelper _authHelper;
public ApproveTaskSectionCompletionCommandHandler(
ITaskSectionRepository taskSectionRepository,
IUnitOfWork unitOfWork,
IAuthHelper authHelper)
{
_taskSectionRepository = taskSectionRepository;
_unitOfWork = unitOfWork;
_authHelper = authHelper;
}
public async Task<OperationResult> Handle(ApproveTaskSectionCompletionCommand request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId()
?? throw new UnAuthorizedException("˜ÇÑÈÑ ÇÍÑÇÒ åæ?Ê äÔÏå ÇÓÊ");
var section = await _taskSectionRepository.GetByIdAsync(request.TaskSectionId, cancellationToken);
if (section == null)
{
return OperationResult.NotFound("ÈÎÔ ãæÑÏ äÙÑ ?ÇÝÊ äÔÏ");
}
if (section.Status != TaskSectionStatus.PendingForCompletion)
{
return OperationResult.Failure("ÝÞØ ÈÎÔ<C38E>åÇ?? ˜å ÏÑ ÇäÊÙÇÑ Ê˜ã?á åÓÊäÏ ÞÇÈá ÊÇ??Ï ?Ç ÑÏ åÓÊäÏ");
}
if (request.IsApproved)
{
section.UpdateStatus(TaskSectionStatus.Completed);
}
else
{
section.UpdateStatus(TaskSectionStatus.Incomplete);
}
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
}

View File

@@ -0,0 +1,14 @@
using FluentValidation;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion;
public class ApproveTaskSectionCompletionCommandValidator : AbstractValidator<ApproveTaskSectionCompletionCommand>
{
public ApproveTaskSectionCompletionCommandValidator()
{
RuleFor(c => c.TaskSectionId)
.NotEmpty()
.NotNull()
.WithMessage("ÔäÇÓå ÈÎÔ äã?<3F>ÊæÇäÏ ÎÇá? ÈÇÔÏ");
}
}

View File

@@ -52,7 +52,10 @@ public class ChangeStatusSectionCommandHandler : IBaseCommandHandler<ChangeStatu
// Going TO InProgress: Check if section has remaining time, then start work
if (!section.HasRemainingTime())
return OperationResult.ValidationError("زمان این بخش به پایان رسیده است");
if (await _taskSectionRepository.HasUserAnyInProgressSectionAsync(section.CurrentAssignedUserId, cancellationToken))
{
return OperationResult.ValidationError("کاربر مورد نظر در حال حاضر بخش دیگری را در وضعیت 'درحال انجام' دارد");
}
section.StartWork();
}
else
@@ -86,9 +89,9 @@ public class ChangeStatusSectionCommandHandler : IBaseCommandHandler<ChangeStatu
var validTransitions = new Dictionary<TaskSectionStatus, List<TaskSectionStatus>>
{
{ TaskSectionStatus.ReadyToStart, [TaskSectionStatus.InProgress] },
{ TaskSectionStatus.InProgress, [TaskSectionStatus.Incomplete, TaskSectionStatus.Completed] },
{ TaskSectionStatus.Incomplete, [TaskSectionStatus.InProgress, TaskSectionStatus.Completed] },
{ TaskSectionStatus.Completed, [TaskSectionStatus.InProgress, TaskSectionStatus.Incomplete] }, // Can return to InProgress or Incomplete
{ TaskSectionStatus.InProgress, [TaskSectionStatus.Incomplete, TaskSectionStatus.PendingForCompletion] },
{ TaskSectionStatus.Incomplete, [TaskSectionStatus.InProgress, TaskSectionStatus.PendingForCompletion] },
{ TaskSectionStatus.PendingForCompletion, [TaskSectionStatus.InProgress, TaskSectionStatus.Incomplete] }, // Can return to InProgress or Incomplete
{ TaskSectionStatus.NotAssigned, [TaskSectionStatus.InProgress, TaskSectionStatus.ReadyToStart] }
};

View File

@@ -4,10 +4,15 @@ using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimeProject;
public record SetTimeProjectCommand(List<SetTimeProjectSectionItem> SectionItems, Guid Id, ProjectHierarchyLevel Level):IBaseCommand;
public record SetTimeProjectCommand(
List<SetTimeProjectSkillItem> SkillItems,
Guid Id,
ProjectHierarchyLevel Level,
bool CascadeToChildren) : IBaseCommand;
public class SetTimeSectionTime
{
public string Description { get; set; }
public int Hours { get; set; }
public int Minutes { get; set; }
}

View File

@@ -6,6 +6,8 @@ using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
using GozareshgirProgramManager.Domain.SkillAgg.Repositories;
using GozareshgirProgramManager.Domain.UserAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimeProject;
@@ -15,21 +17,33 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
private readonly IProjectPhaseRepository _projectPhaseRepository;
private readonly IProjectTaskRepository _projectTaskRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IAuthHelper _authHelper;
private readonly IUserRepository _userRepository;
private readonly ISkillRepository _skillRepository;
private readonly IPhaseSectionRepository _phaseSectionRepository;
private readonly IProjectSectionRepository _projectSectionRepository;
private long? _userId;
private readonly ITaskSectionRepository _taskSectionRepository;
public SetTimeProjectCommandHandler(
IProjectRepository projectRepository,
IProjectPhaseRepository projectPhaseRepository,
IProjectTaskRepository projectTaskRepository,
IUnitOfWork unitOfWork, IAuthHelper authHelper)
IUnitOfWork unitOfWork, IAuthHelper authHelper,
IUserRepository userRepository, ISkillRepository skillRepository,
IPhaseSectionRepository phaseSectionRepository,
IProjectSectionRepository projectSectionRepository,
ITaskSectionRepository taskSectionRepository)
{
_projectRepository = projectRepository;
_projectPhaseRepository = projectPhaseRepository;
_projectTaskRepository = projectTaskRepository;
_unitOfWork = unitOfWork;
_authHelper = authHelper;
_userRepository = userRepository;
_skillRepository = skillRepository;
_phaseSectionRepository = phaseSectionRepository;
_projectSectionRepository = projectSectionRepository;
_taskSectionRepository = taskSectionRepository;
_userId = authHelper.GetCurrentUserId();
}
@@ -37,6 +51,10 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
{
switch (request.Level)
{
case ProjectHierarchyLevel.Project:
return await AssignProject(request);
case ProjectHierarchyLevel.Phase:
return await AssignProjectPhase(request);
case ProjectHierarchyLevel.Task:
return await SetTimeForProjectTask(request, cancellationToken);
default:
@@ -44,67 +62,229 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
}
}
private async Task<OperationResult> SetTimeForProject(SetTimeProjectCommand request,
CancellationToken cancellationToken)
private async Task<OperationResult> AssignProject(SetTimeProjectCommand request)
{
var project = await _projectRepository.GetWithFullHierarchyAsync(request.Id);
if (project == null)
if (project is null)
{
return OperationResult.NotFound("پروژه یافت نشد");
return OperationResult.NotFound("<22><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD>");
}
long? addedByUserId = _userId;
var skillItems = request.SkillItems.Where(x=>x.UserId is > 0).ToList();
// حذف ProjectSections که در validSkills نیستند
var validSkillIds = skillItems.Select(x => x.SkillId).ToList();
var sectionsToRemove = project.ProjectSections
.Where(s => !validSkillIds.Contains(s.SkillId))
.ToList();
foreach (var section in sectionsToRemove)
{
project.RemoveProjectSection(section.SkillId);
}
// تخصیص در سطح پروژه
foreach (var item in skillItems)
{
var skill = await _skillRepository.GetByIdAsync(item.SkillId);
if (skill is null)
{
return OperationResult.NotFound($"مهارت با شناسه {item.SkillId} یافت نشد");
}
// تنظیم زمان برای تمام sections در تمام فازها و تسک‌های پروژه
// بررسی و به‌روزرسانی یا اضافه کردن ProjectSection
var existingSection = project.ProjectSections.FirstOrDefault(s => s.SkillId == item.SkillId);
if (existingSection != null)
{
// اگر وجود داشت، فقط userId را به‌روزرسانی کن
existingSection.UpdateUser(item.UserId.Value);
}
else
{
// اگر وجود نداشت، اضافه کن
var newSection = new ProjectSection(project.Id, item.UserId.Value, item.SkillId);
await _projectSectionRepository.CreateAsync(newSection);
}
}
// حالا برای تمام فازها و تسک‌ها cascade کن
foreach (var phase in project.Phases)
{
foreach (var task in phase.Tasks)
// اگر CascadeToChildren true است یا فاز override ندارد
if (request.CascadeToChildren || !phase.HasAssignmentOverride)
{
foreach (var section in task.Sections)
// حذف PhaseSections که در validSkills نیستند
var phaseSectionsToRemove = phase.PhaseSections
.Where(s => !validSkillIds.Contains(s.SkillId))
.ToList();
foreach (var section in phaseSectionsToRemove)
{
var sectionItem = request.SectionItems.FirstOrDefault(si => si.SectionId == section.Id);
if (sectionItem != null)
phase.RemovePhaseSection(section.SkillId);
}
// برای phase هم باید sectionها را به‌روزرسانی کنیم
foreach (var item in skillItems )
{
var existingSection = phase.PhaseSections.FirstOrDefault(s => s.SkillId == item.SkillId);
if (existingSection != null)
{
SetSectionTime(section, sectionItem, addedByUserId);
existingSection.Update(item.UserId.Value, item.SkillId);
}
else
{
var newPhaseSection = new PhaseSection(phase.Id, item.UserId.Value, item.SkillId);
await _phaseSectionRepository.CreateAsync(newPhaseSection);
}
}
foreach (var task in phase.Tasks)
{
// اگر CascadeToChildren true است یا تسک override ندارد
if (request.CascadeToChildren || !task.HasAssignmentOverride)
{
// حذف TaskSections که در validSkills نیستند
var taskSectionsToRemove = task.Sections
.Where(s => !validSkillIds.Contains(s.SkillId))
.ToList();
foreach (var section in taskSectionsToRemove)
{
task.RemoveSection(section.Id);
}
foreach (var item in skillItems)
{
var section = task.Sections.FirstOrDefault(s => s.SkillId == item.SkillId);
if (section != null)
{
// استفاده از TransferToUser
if (section.CurrentAssignedUserId != item.UserId)
{
if (section.CurrentAssignedUserId > 0)
{
section.TransferToUser(section.CurrentAssignedUserId, item.UserId.Value);
}
else
{
section.AssignToUser(item.UserId.Value);
}
}
}
else
{
var newTaskSection = new TaskSection(task.Id, item.SkillId, item.UserId.Value);
await _taskSectionRepository.CreateAsync(newTaskSection);
}
}
}
}
}
}
await _unitOfWork.SaveChangesAsync(cancellationToken);
await _unitOfWork.SaveChangesAsync();
return OperationResult.Success();
}
private async Task<OperationResult> SetTimeForProjectPhase(SetTimeProjectCommand request,
CancellationToken cancellationToken)
private async Task<OperationResult> AssignProjectPhase(SetTimeProjectCommand request)
{
var phase = await _projectPhaseRepository.GetWithTasksAsync(request.Id);
if (phase == null)
if (phase is null)
{
return OperationResult.NotFound("فاز پروژه یافت نشد");
return OperationResult.NotFound("<22><><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD>");
}
long? addedByUserId = _userId;
// تخصیص در سطح فاز
foreach (var item in request.SkillItems)
{
var skill = await _skillRepository.GetByIdAsync(item.SkillId);
if (skill is null)
{
return OperationResult.NotFound($"مهارت با شناسه {item.SkillId} یافت نشد");
}
}
// تنظیم زمان برای تمام sections در تمام تسک‌های این فاز
// علامت‌گذاری که این فاز نسبت به parent متمایز است
phase.MarkAsOverridden();
var skillItems = request.SkillItems.Where(x=>x.UserId is > 0).ToList();
// حذف PhaseSections که در validSkills نیستند
var validSkillIds = skillItems.Select(x => x.SkillId).ToList();
var sectionsToRemove = phase.PhaseSections
.Where(s => !validSkillIds.Contains(s.SkillId))
.ToList();
foreach (var section in sectionsToRemove)
{
phase.RemovePhaseSection(section.SkillId);
}
// به‌روزرسانی یا اضافه کردن PhaseSection
foreach (var item in skillItems)
{
var existingSection = phase.PhaseSections.FirstOrDefault(s => s.SkillId == item.SkillId);
if (existingSection != null)
{
// اگر وجود داشت، فقط userId را به‌روزرسانی کن
existingSection.Update(item.UserId!.Value, item.SkillId);
}
else
{
// اگر وجود نداشت، اضافه کن
var newPhaseSection = new PhaseSection(phase.Id, item.UserId!.Value, item.SkillId);
await _phaseSectionRepository.CreateAsync(newPhaseSection);
}
}
// cascade به تمام تسک‌ها
foreach (var task in phase.Tasks)
{
foreach (var section in task.Sections)
// اگر CascadeToChildren true است یا تسک override ندارد
if (request.CascadeToChildren || !task.HasAssignmentOverride)
{
var sectionItem = request.SectionItems.FirstOrDefault(si => si.SectionId == section.Id);
if (sectionItem != null)
// حذف TaskSections که در validSkills نیستند
var taskSectionsToRemove = task.Sections
.Where(s => !validSkillIds.Contains(s.SkillId))
.ToList();
foreach (var section in taskSectionsToRemove)
{
SetSectionTime(section, sectionItem, addedByUserId);
task.RemoveSection(section.Id);
}
foreach (var item in skillItems)
{
var section = task.Sections.FirstOrDefault(s => s.SkillId == item.SkillId);
if (section != null)
{
// استفاده از TransferToUser
if (section.CurrentAssignedUserId != item.UserId)
{
if (section.CurrentAssignedUserId > 0)
{
section.TransferToUser(section.CurrentAssignedUserId, item.UserId!.Value);
}
else
{
section.AssignToUser(item.UserId!.Value);
}
}
}
else
{
var newTaskSection = new TaskSection(task.Id, item.SkillId, item.UserId!.Value);
await _taskSectionRepository.CreateAsync(newTaskSection);
}
}
}
}
await _unitOfWork.SaveChangesAsync(cancellationToken);
await _unitOfWork.SaveChangesAsync();
return OperationResult.Success();
}
private async Task<OperationResult> SetTimeForProjectTask(SetTimeProjectCommand request,
CancellationToken cancellationToken)
{
@@ -116,21 +296,60 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
long? addedByUserId = _userId;
// تنظیم زمان مستقیماً برای sections این تسک
foreach (var section in task.Sections)
var validSkills = request.SkillItems
.Where(x=>x.UserId is > 0).ToList();
// حذف سکشن‌هایی که در validSkills نیستند
var validSkillIds = validSkills.Select(x => x.SkillId).ToList();
var sectionsToRemove = task.Sections
.Where(s => !validSkillIds.Contains(s.SkillId))
.ToList();
foreach (var sectionToRemove in sectionsToRemove)
{
var sectionItem = request.SectionItems.FirstOrDefault(si => si.SectionId == section.Id);
if (sectionItem != null)
{
SetSectionTime(section, sectionItem, addedByUserId);
}
task.RemoveSection(sectionToRemove.Id);
}
foreach (var skillItem in validSkills)
{
var section = task.Sections.FirstOrDefault(s => s.SkillId == skillItem.SkillId);
if (!_userRepository.Exists(x=>x.Id == skillItem.UserId!.Value))
{
throw new BadRequestException("کاربر با شناسه یافت نشد.");
}
if (section == null)
{
var taskSection = new TaskSection(task.Id,
skillItem.SkillId, skillItem.UserId!.Value);
task.AddSection(taskSection);
section = taskSection;
}
else
{
if (section.CurrentAssignedUserId != skillItem.UserId)
{
if (section.CurrentAssignedUserId > 0)
{
section.TransferToUser(section.CurrentAssignedUserId, skillItem.UserId!.Value);
}
else
{
section.AssignToUser(skillItem.UserId!.Value);
}
}
}
SetSectionTime(section, skillItem, addedByUserId);
}
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
private void SetSectionTime(TaskSection section, SetTimeProjectSectionItem sectionItem, long? addedByUserId)
private void SetSectionTime(TaskSection section, SetTimeProjectSkillItem sectionItem, long? addedByUserId)
{
var initData = sectionItem.InitData;
var initialTime = TimeSpan.FromHours(initData.Hours);
@@ -147,7 +366,7 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
// افزودن زمان‌های اضافی
foreach (var additionalTime in sectionItem.AdditionalTime)
{
var additionalTimeSpan = TimeSpan.FromHours(additionalTime.Hours);
var additionalTimeSpan = TimeSpan.FromHours(additionalTime.Hours).Add(TimeSpan.FromMinutes(additionalTime.Minutes));
section.AddAdditionalTime(additionalTimeSpan, additionalTime.Description, addedByUserId);
}
}

View File

@@ -13,19 +13,15 @@ public class SetTimeProjectCommandValidator:AbstractValidator<SetTimeProjectComm
.NotNull()
.WithMessage("شناسه پروژه نمی‌تواند خالی باشد.");
RuleForEach(x => x.SectionItems)
.SetValidator(command => new SetTimeProjectSectionItemValidator());
RuleFor(x => x.SectionItems)
.Must(sectionItems => sectionItems.Any(si => si.InitData?.Hours > 0))
.WithMessage("حداقل یکی از بخش‌ها باید مقدار ساعت معتبری داشته باشد.");
RuleForEach(x => x.SkillItems)
.SetValidator(command => new SetTimeProjectSkillItemValidator());
}
}
public class SetTimeProjectSectionItemValidator:AbstractValidator<SetTimeProjectSectionItem>
public class SetTimeProjectSkillItemValidator:AbstractValidator<SetTimeProjectSkillItem>
{
public SetTimeProjectSectionItemValidator()
public SetTimeProjectSkillItemValidator()
{
RuleFor(x=>x.SectionId)
RuleFor(x=>x.SkillId)
.NotEmpty()
.NotNull()
.WithMessage("شناسه بخش نمی‌تواند خالی باشد.");
@@ -47,6 +43,18 @@ public class AdditionalTimeDataValidator: AbstractValidator<SetTimeSectionTime>
.GreaterThanOrEqualTo(0)
.WithMessage("ساعت نمی‌تواند منفی باشد.");
RuleFor(x => x.Hours)
.LessThan(1_000)
.WithMessage("ساعت باید کمتر از 1000 باشد.");
RuleFor(x => x.Minutes)
.GreaterThanOrEqualTo(0)
.WithMessage("دقیقه نمی‌تواند منفی باشد.");
RuleFor(x => x.Minutes)
.LessThan(60)
.WithMessage("دقیقه باید بین 0 تا 59 باشد.");
RuleFor(x=>x.Description)
.MaximumLength(500)
.WithMessage("توضیحات نمی‌تواند بیشتر از 500 کاراکتر باشد.");

View File

@@ -2,9 +2,10 @@ using GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimePro
namespace GozareshgirProgramManager.Application.Modules.Projects.DTOs;
public class SetTimeProjectSectionItem
public class SetTimeProjectSkillItem
{
public Guid SectionId { get; set; }
public Guid SkillId { get; set; }
public long? UserId { get; set; }
public SetTimeSectionTime InitData { get; set; }
public List<SetTimeSectionTime> AdditionalTime { get; set; } = [];
}

View File

@@ -0,0 +1,11 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
/// <summary>
/// درخواست جستجو در سراسر سلسله‌مراتب پروژه (پروژه، فاز، تسک).
/// نتایج با اطلاعات مسیر سلسله‌مراتب برای پشتیبانی از ناوبری درخت در رابط کاربری بازگردانده می‌شود.
/// </summary>
public record GetProjectSearchQuery(
string SearchQuery) : IBaseQuery<GetProjectSearchResponse>;

View File

@@ -0,0 +1,132 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
/// <summary>
/// Handler برای درخواست جستجوی سراسری در سلسله‌مراتب پروژه.
/// این handler در تمام سطح‌های پروژه، فاز و تسک جستجو می‌کند و از تمام فیلدهای متنی (نام، توضیحات) استفاده می‌کند.
/// همچنین در زیرمجموعه‌های هر سطح (ProjectSections، PhaseSections، TaskSections) جستجو می‌کند.
/// </summary>
public class GetProjectSearchQueryHandler : IBaseQueryHandler<GetProjectSearchQuery, GetProjectSearchResponse>
{
private readonly IProgramManagerDbContext _context;
private const int MaxResults = 50;
public GetProjectSearchQueryHandler(IProgramManagerDbContext context)
{
_context = context;
}
public async Task<OperationResult<GetProjectSearchResponse>> Handle(
GetProjectSearchQuery request,
CancellationToken cancellationToken)
{
var searchQuery = request.SearchQuery.ToLower();
var results = new List<ProjectHierarchySearchResultDto>();
// جستجو در پروژه‌ها و ProjectSections
var projects = await SearchProjects(searchQuery, cancellationToken);
results.AddRange(projects);
// جستجو در فازها و PhaseSections
var phases = await SearchPhases(searchQuery, cancellationToken);
results.AddRange(phases);
// جستجو در تسک‌ها و TaskSections
var tasks = await SearchTasks(searchQuery, cancellationToken);
results.AddRange(tasks);
// مرتب‌سازی نتایج: ابتدا بر اساس سطح سلسله‌مراتب (پروژه → فاز → تسک)، سپس بر اساس نام
var sortedResults = results
.OrderBy(r => r.Level)
.ThenBy(r => r.Title)
.Take(MaxResults)
.ToList();
var response = new GetProjectSearchResponse(sortedResults);
return OperationResult<GetProjectSearchResponse>.Success(response);
}
/// <summary>
/// جستجو در جدول پروژه‌ها (نام، توضیحات) و ProjectSections (نام مهارت، توضیحات اولیه)
/// </summary>
private async Task<List<ProjectHierarchySearchResultDto>> SearchProjects(
string searchQuery,
CancellationToken cancellationToken)
{
var projects = await _context.Projects
.Where(p =>
p.Name.ToLower().Contains(searchQuery) ||
(p.Description != null && p.Description.ToLower().Contains(searchQuery)))
.Select(p => new ProjectHierarchySearchResultDto
{
Id = p.Id,
Title = p.Name,
Level = ProjectHierarchyLevel.Project,
ProjectId = null,
PhaseId = null
})
.ToListAsync(cancellationToken);
return projects;
}
/// <summary>
/// جستجو در جدول فازهای پروژه (نام، توضیحات) و PhaseSections
/// </summary>
private async Task<List<ProjectHierarchySearchResultDto>> SearchPhases(
string searchQuery,
CancellationToken cancellationToken)
{
var phases = await _context.ProjectPhases
.Where(ph =>
ph.Name.ToLower().Contains(searchQuery) ||
(ph.Description != null && ph.Description.ToLower().Contains(searchQuery)))
.Select(ph => new ProjectHierarchySearchResultDto
{
Id = ph.Id,
Title = ph.Name,
Level = ProjectHierarchyLevel.Phase,
ProjectId = ph.ProjectId,
PhaseId = null
})
.ToListAsync(cancellationToken);
return phases;
}
/// <summary>
/// جستجو در جدول تسک‌های پروژه (نام، توضیحات) و TaskSections (نام مهارت، توضیح اولیه، اطلاعات اضافی)
/// </summary>
private async Task<List<ProjectHierarchySearchResultDto>> SearchTasks(
string searchQuery,
CancellationToken cancellationToken)
{
var tasks = await _context.ProjectTasks
.Include(t => t.Sections)
.Include(t => t.Phase)
.Where(t =>
t.Name.ToLower().Contains(searchQuery) ||
(t.Description != null && t.Description.ToLower().Contains(searchQuery)) ||
t.Sections.Any(s =>
(s.InitialDescription != null && s.InitialDescription.ToLower().Contains(searchQuery)) ||
s.AdditionalTimes.Any(at => at.Reason != null && at.Reason.ToLower().Contains(searchQuery))))
.Select(t => new ProjectHierarchySearchResultDto
{
Id = t.Id,
Title = t.Name,
Level = ProjectHierarchyLevel.Task,
ProjectId = t.Phase.ProjectId,
PhaseId = t.PhaseId
})
.ToListAsync(cancellationToken);
return tasks;
}
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
/// <summary>
/// اعتبارسنج برای درخواست جستجوی سراسری
/// </summary>
public class GetProjectSearchQueryValidator : AbstractValidator<GetProjectSearchQuery>
{
public GetProjectSearchQueryValidator()
{
RuleFor(x => x.SearchQuery)
.NotEmpty().WithMessage("متن جستجو نمی‌تواند خالی باشد.")
.MinimumLength(2).WithMessage("متن جستجو باید حداقل 2 حرف باشد.")
.MaximumLength(500).WithMessage("متن جستجو نمی‌تواند بیش از 500 حرف باشد.");
}
}

View File

@@ -0,0 +1,8 @@
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
/// <summary>
/// پوسته‌ی پاسخ برای نتایج جستجوی سراسری
/// </summary>
public record GetProjectSearchResponse(
List<ProjectHierarchySearchResultDto> Results);

View File

@@ -0,0 +1,36 @@
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
/// <summary>
/// DTO برای نتایج جستجوی سراسری در سلسله‌مراتب پروژه.
/// حاوی اطلاعات کافی برای بازسازی مسیر سلسله‌مراتب و بسط درخت در رابط کاربری است.
/// </summary>
public record ProjectHierarchySearchResultDto
{
/// <summary>
/// شناسه آیتم (پروژه، فاز یا تسک)
/// </summary>
public Guid Id { get; init; }
/// <summary>
/// نام/عنوان آیتم
/// </summary>
public string Title { get; init; } = string.Empty;
/// <summary>
/// سطح سلسله‌مراتب این آیتم
/// </summary>
public ProjectHierarchyLevel Level { get; init; }
/// <summary>
/// شناسه پروژه - همیشه برای فاز و تسک پر شده است، برای پروژه با شناسه خود پر می‌شود
/// </summary>
public Guid? ProjectId { get; init; }
/// <summary>
/// شناسه فاز - فقط برای تسک پر شده است، برای پروژه و فاز خالی است
/// </summary>
public Guid? PhaseId { get; init; }
}

View File

@@ -11,6 +11,8 @@ public record GetProjectListDto
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; }
}

View File

@@ -53,17 +53,19 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
.OrderByDescending(p => p.CreationDate)
.ToListAsync(cancellationToken);
var result = new List<GetProjectListDto>();
foreach (var project in projects)
{
var percentage = await CalculateProjectPercentage(project, cancellationToken);
var (percentage, totalTime) = await CalculateProjectPercentage(project, cancellationToken);
result.Add(new GetProjectListDto
{
Id = project.Id,
Name = project.Name,
Level = ProjectHierarchyLevel.Project,
ParentId = null,
Percentage = percentage
Percentage = percentage,
TotalHours = (int)totalTime.TotalHours,
Minutes = totalTime.Minutes,
});
}
@@ -86,14 +88,16 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
foreach (var phase in phases)
{
var percentage = await CalculatePhasePercentage(phase, cancellationToken);
var (percentage, totalTime) = await CalculatePhasePercentage(phase, cancellationToken);
result.Add(new GetProjectListDto
{
Id = phase.Id,
Name = phase.Name,
Level = ProjectHierarchyLevel.Phase,
ParentId = phase.ProjectId,
Percentage = percentage
Percentage = percentage,
TotalHours = (int)totalTime.TotalHours,
Minutes = totalTime.Minutes,
});
}
@@ -116,14 +120,16 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
foreach (var task in tasks)
{
var percentage = await CalculateTaskPercentage(task, cancellationToken);
var (percentage, totalTime) = await CalculateTaskPercentage(task, cancellationToken);
result.Add(new GetProjectListDto
{
Id = task.Id,
Name = task.Name,
Level = ProjectHierarchyLevel.Task,
ParentId = task.PhaseId,
Percentage = percentage
Percentage = percentage,
TotalHours = (int)totalTime.TotalHours,
Minutes = totalTime.Minutes
});
}
@@ -211,7 +217,7 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
}
}
private async Task<int> CalculateProjectPercentage(Project project, CancellationToken cancellationToken)
private async Task<(int Percentage, TimeSpan TotalTime)> CalculateProjectPercentage(Project project, CancellationToken cancellationToken)
{
// گرفتن تمام فازهای پروژه
var phases = await _context.ProjectPhases
@@ -219,20 +225,24 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
.ToListAsync(cancellationToken);
if (!phases.Any())
return 0;
return (0, TimeSpan.Zero);
// محاسبه درصد هر فاز و میانگین‌گیری
var phasePercentages = new List<int>();
var totalTime = TimeSpan.Zero;
foreach (var phase in phases)
{
var phasePercentage = await CalculatePhasePercentage(phase, cancellationToken);
var (phasePercentage, phaseTime) = await CalculatePhasePercentage(phase, cancellationToken);
phasePercentages.Add(phasePercentage);
totalTime += phaseTime;
}
return phasePercentages.Any() ? (int)phasePercentages.Average() : 0;
var averagePercentage = phasePercentages.Any() ? (int)phasePercentages.Average() : 0;
return (averagePercentage, totalTime);
}
private async Task<int> CalculatePhasePercentage(ProjectPhase phase, CancellationToken cancellationToken)
private async Task<(int Percentage, TimeSpan TotalTime)> CalculatePhasePercentage(ProjectPhase phase, CancellationToken cancellationToken)
{
// گرفتن تمام تسک‌های فاز
var tasks = await _context.ProjectTasks
@@ -240,55 +250,66 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
.ToListAsync(cancellationToken);
if (!tasks.Any())
return 0;
return (0, TimeSpan.Zero);
// محاسبه درصد هر تسک و میانگین‌گیری
var taskPercentages = new List<int>();
var totalTime = TimeSpan.Zero;
foreach (var task in tasks)
{
var taskPercentage = await CalculateTaskPercentage(task, cancellationToken);
var (taskPercentage, taskTime) = await CalculateTaskPercentage(task, cancellationToken);
taskPercentages.Add(taskPercentage);
totalTime += taskTime;
}
return taskPercentages.Any() ? (int)taskPercentages.Average() : 0;
var averagePercentage = taskPercentages.Any() ? (int)taskPercentages.Average() : 0;
return (averagePercentage, totalTime);
}
private async Task<int> CalculateTaskPercentage(ProjectTask task, CancellationToken cancellationToken)
private async Task<(int Percentage, TimeSpan TotalTime)> CalculateTaskPercentage(ProjectTask task, CancellationToken cancellationToken)
{
// گرفتن تمام سکشن‌های تسک با activities
var sections = await _context.TaskSections
.Include(s => s.Activities)
.Include(x=>x.AdditionalTimes)
.Where(s => s.TaskId == task.Id)
.ToListAsync(cancellationToken);
if (!sections.Any())
return 0;
return (0, TimeSpan.Zero);
// محاسبه درصد هر سکشن و میانگین‌گیری
var sectionPercentages = new List<int>();
var totalTime = TimeSpan.Zero;
foreach (var section in sections)
{
var sectionPercentage = CalculateSectionPercentage(section);
var (sectionPercentage, sectionTime) = CalculateSectionPercentage(section);
sectionPercentages.Add(sectionPercentage);
totalTime += sectionTime;
}
return sectionPercentages.Any() ? (int)sectionPercentages.Average() : 0;
var averagePercentage = sectionPercentages.Any() ? (int)sectionPercentages.Average() : 0;
return (averagePercentage, totalTime);
}
private static int CalculateSectionPercentage(TaskSection section)
private static (int Percentage, TimeSpan TotalTime) CalculateSectionPercentage(TaskSection section)
{
// محاسبه کل زمان تخمین زده شده (اولیه + اضافی)
var totalEstimatedHours = section.FinalEstimatedHours.TotalHours;
if (totalEstimatedHours <= 0)
return 0;
// محاسبه کل زمان صرف شده از activities
var totalSpentHours = section.Activities.Sum(a => a.GetTimeSpent().TotalHours);
var totalSpentTime = TimeSpan.FromHours(section.Activities.Sum(a => a.GetTimeSpent().TotalHours));
if (totalEstimatedHours <= 0)
return (0, section.FinalEstimatedHours);
var totalSpentHours = totalSpentTime.TotalHours;
// محاسبه درصد (حداکثر 100%)
var percentage = (totalSpentHours / totalEstimatedHours) * 100;
return Math.Min((int)Math.Round(percentage), 100);
return (Math.Min((int)Math.Round(percentage), 100), section.FinalEstimatedHours);
}
}

View File

@@ -53,67 +53,82 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
.ToDictionaryAsync(x => x.Id, x => x.FullName, cancellationToken);
var result = data.Select(x =>
{
// محاسبه یکبار برای هر Activity و Cache کردن نتیجه
var activityTimeData = x.Activities.Select(a =>
var result = data
.Select(x =>
{
var timeSpent = a.GetTimeSpent();
return new
// محاسبه یکبار برای هر Activity و Cache کردن نتیجه
var activityTimeData = x.Activities.Select(a =>
{
Activity = a,
TimeSpent = timeSpent,
TotalSeconds = timeSpent.TotalSeconds,
FormattedTime = timeSpent.ToString(@"hh\:mm")
};
}).ToList();
// ادغام پشت سر هم فعالیت‌های یک کاربر
var mergedHistories = new List<ProjectProgressHistoryDto>();
foreach (var activityData in activityTimeData)
{
var lastHistory = mergedHistories.LastOrDefault();
// اگر آخرین history برای همین کاربر باشد، زمان‌ها را جمع می‌کنیم
if (lastHistory != null && lastHistory.UserId == activityData.Activity.UserId)
{
var totalTimeSpan = lastHistory.WorkedTimeSpan + activityData.TimeSpent;
lastHistory.WorkedTimeSpan = totalTimeSpan;
lastHistory.WorkedTime = totalTimeSpan.ToString(@"hh\:mm");
}
else
{
// در غیر این صورت، یک history جدید اضافه می‌کنیم
mergedHistories.Add(new ProjectProgressHistoryDto()
var timeSpent = a.GetTimeSpent();
return new
{
UserId = activityData.Activity.UserId,
IsCurrentUser = activityData.Activity.UserId == currentUserId,
Name = users.GetValueOrDefault(activityData.Activity.UserId, "ناشناس"),
WorkedTime = activityData.FormattedTime,
WorkedTimeSpan = activityData.TimeSpent,
});
}
}
Activity = a,
TimeSpent = timeSpent,
TotalSeconds = timeSpent.TotalSeconds,
FormattedTime = timeSpent.ToString(@"hh\:mm")
};
}).ToList();
return new ProjectBoardListResponse()
{
Id = x.Id,
PhaseName = x.Task.Phase.Name,
ProjectName = x.Task.Phase.Project.Name,
TaskName = x.Task.Name,
SectionStatus = x.Status,
Progress = new ProjectProgressDto()
// ادغام پشت سر هم فعالیت‌های یک کاربر
var mergedHistories = new List<ProjectProgressHistoryDto>();
foreach (var activityData in activityTimeData)
{
CompleteSecond = x.FinalEstimatedHours.TotalSeconds,
CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds),
Histories = mergedHistories
},
OriginalUser = users.GetValueOrDefault(x.OriginalAssignedUserId, "ناشناس"),
AssignedUser = x.CurrentAssignedUserId == x.OriginalAssignedUserId ? null
: users.GetValueOrDefault(x.CurrentAssignedUserId, "ناشناس"),
SkillName = x.Skill?.Name??"-",
};
}).ToList();
var lastHistory = mergedHistories.LastOrDefault();
// اگر آخرین history برای همین کاربر باشد، زمان‌ها را جمع می‌کنیم
if (lastHistory != null && lastHistory.UserId == activityData.Activity.UserId)
{
var totalTimeSpan = lastHistory.WorkedTimeSpan + activityData.TimeSpent;
lastHistory.WorkedTimeSpan = totalTimeSpan;
lastHistory.WorkedTime = totalTimeSpan.ToString(@"hh\:mm");
}
else
{
// در غیر این صورت، یک history جدید اضافه می‌کنیم
mergedHistories.Add(new ProjectProgressHistoryDto()
{
UserId = activityData.Activity.UserId,
IsCurrentUser = activityData.Activity.UserId == currentUserId,
Name = users.GetValueOrDefault(activityData.Activity.UserId, "ناشناس"),
WorkedTime = activityData.FormattedTime,
WorkedTimeSpan = activityData.TimeSpent,
});
}
}
mergedHistories = mergedHistories.OrderByDescending(h => h.IsCurrentUser).ToList();
return new ProjectBoardListResponse()
{
Id = x.Id,
PhaseName = x.Task.Phase.Name,
ProjectName = x.Task.Phase.Project.Name,
TaskName = x.Task.Name,
SectionStatus = x.Status,
Progress = new ProjectProgressDto()
{
CompleteSecond = x.FinalEstimatedHours.TotalSeconds,
CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds),
Histories = mergedHistories
},
OriginalUser = users.GetValueOrDefault(x.OriginalAssignedUserId, "ناشناس"),
AssignedUser = x.CurrentAssignedUserId == x.OriginalAssignedUserId ? null
: users.GetValueOrDefault(x.CurrentAssignedUserId, "ناشناس"),
SkillName = x.Skill?.Name??"-",
TaskId = x.TaskId
};
})
.OrderByDescending(r =>
{
// اگر AssignedUser null نباشد، بررسی کن که برابر current user هست یا نه
if (r.AssignedUser != null)
{
return users.FirstOrDefault(u => u.Value == r.AssignedUser).Key == currentUserId;
}
// اگر AssignedUser null بود، از OriginalUser بررسی کن
return users.FirstOrDefault(u => u.Value == r.OriginalUser).Key == currentUserId;
})
.ToList();
return OperationResult<List<ProjectBoardListResponse>>.Success(result);
}

View File

@@ -13,6 +13,8 @@ public class ProjectBoardListResponse
public string? AssignedUser { get; set; }
public string OriginalUser { get; set; }
public string SkillName { get; set; }
public Guid TaskId { get; set; }
}
public class ProjectProgressDto

View File

@@ -3,21 +3,22 @@ using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails;
public record ProjectSetTimeDetailsQuery(Guid TaskId)
public record ProjectSetTimeDetailsQuery(Guid Id, ProjectHierarchyLevel Level)
: IBaseQuery<ProjectSetTimeResponse>;
public record ProjectSetTimeResponse(
List<ProjectSetTimeResponseSections> SectionItems,
List<ProjectSetTimeResponseSkill> SkillItems,
Guid Id,
ProjectHierarchyLevel Level);
public record ProjectSetTimeResponseSections
public record ProjectSetTimeResponseSkill
{
public Guid SkillId { get; init; }
public string SkillName { get; init; }
public string UserName { get; init; }
public int InitialTime { get; set; }
public long UserId { get; set; }
public string UserFullName { get; init; }
public int InitialHours { get; set; }
public int InitialMinutes { get; set; }
public string InitialDescription { get; set; }
public int TotalEstimateTime { get; init; }
public int TotalAdditionalTime { get; init; }
public string InitCreationTime { get; init; }
public List<ProjectSetTimeResponseSectionAdditionalTime> AdditionalTimes { get; init; }
public Guid SectionId { get; set; }
@@ -25,6 +26,8 @@ public record ProjectSetTimeResponseSections
public class ProjectSetTimeResponseSectionAdditionalTime
{
public int Time { get; init; }
public int Hours { get; init; }
public int Minutes { get; init; }
public string Description { get; init; }
public string CreationDate { get; set; }
}

View File

@@ -22,17 +22,30 @@ public class ProjectSetTimeDetailsQueryHandler
public async Task<OperationResult<ProjectSetTimeResponse>> Handle(ProjectSetTimeDetailsQuery request,
CancellationToken cancellationToken)
{
return request.Level switch
{
ProjectHierarchyLevel.Task => await GetTaskSetTimeDetails(request.Id, request.Level, cancellationToken),
ProjectHierarchyLevel.Phase => await GetPhaseSetTimeDetails(request.Id, request.Level, cancellationToken),
ProjectHierarchyLevel.Project => await GetProjectSetTimeDetails(request.Id, request.Level, cancellationToken),
_ => OperationResult<ProjectSetTimeResponse>.Failure("سطح معادل نامعتبر است")
};
}
private async Task<OperationResult<ProjectSetTimeResponse>> GetTaskSetTimeDetails(Guid id, ProjectHierarchyLevel level,
CancellationToken cancellationToken)
{
var task = await _context.ProjectTasks
.Where(p => p.Id == request.TaskId)
.Where(p => p.Id == id)
.Include(x => x.Sections)
.ThenInclude(x => x.AdditionalTimes).AsNoTracking()
.FirstOrDefaultAsync(cancellationToken);
if (task == null)
{
return OperationResult<ProjectSetTimeResponse>.NotFound("Project not found");
return OperationResult<ProjectSetTimeResponse>.NotFound("تسک یافت نشد");
}
var userIds = task.Sections.Select(x => x.OriginalAssignedUserId)
.Distinct().ToList();
@@ -40,40 +53,142 @@ public class ProjectSetTimeDetailsQueryHandler
.Where(x => userIds.Contains(x.Id))
.AsNoTracking()
.ToListAsync(cancellationToken);
var skillIds = task.Sections.Select(x => x.SkillId)
.Distinct().ToList();
var skills = await _context.Skills
.Where(x => skillIds.Contains(x.Id))
var skills = await _context.Skills
.AsNoTracking()
.ToListAsync(cancellationToken);
var res = new ProjectSetTimeResponse(
task.Sections.Select(ts =>
skills.Select(skill =>
{
var section = task.Sections
.FirstOrDefault(x => x.SkillId == skill.Id);
var user = users.FirstOrDefault(x => x.Id == section?.OriginalAssignedUserId);
return new ProjectSetTimeResponseSkill
{
var user = users.FirstOrDefault(x => x.Id == ts.OriginalAssignedUserId);
var skill = skills.FirstOrDefault(x => x.Id == ts.SkillId);
return new ProjectSetTimeResponseSections
{
AdditionalTimes = ts.AdditionalTimes
.Select(x => new ProjectSetTimeResponseSectionAdditionalTime
{
Description = x.Reason ?? "",
Time = (int)x.Hours.TotalHours
}).ToList(),
InitCreationTime = ts.CreationDate.ToFarsi(),
SkillName = skill?.Name ?? "",
TotalAdditionalTime = (int)ts.GetTotalAdditionalTime().TotalHours,
TotalEstimateTime = (int)ts.FinalEstimatedHours.TotalHours,
UserName = user?.UserName ?? "",
SectionId = ts.Id,
InitialDescription = ts.InitialDescription ?? "",
InitialTime = (int)ts.InitialEstimatedHours.TotalHours
};
}).ToList(),
task.Id,
ProjectHierarchyLevel.Task);
AdditionalTimes = section?.AdditionalTimes
.Select(x => new ProjectSetTimeResponseSectionAdditionalTime
{
Description = x.Reason ?? "",
Hours = (int)x.Hours.TotalHours,
Minutes = x.Hours.Minutes,
CreationDate = x.CreationDate.ToFarsi()
}).OrderBy(x => x.CreationDate).ToList() ?? [],
InitCreationTime = section?.CreationDate.ToFarsi() ?? "",
SkillName = skill.Name ?? "",
UserFullName = user?.FullName ?? "",
SectionId = section?.Id ?? Guid.Empty,
InitialDescription = section?.InitialDescription ?? "",
InitialHours = (int)(section?.InitialEstimatedHours.TotalHours ?? 0),
InitialMinutes = section?.InitialEstimatedHours.Minutes ?? 0,
UserId = section?.OriginalAssignedUserId ?? 0,
SkillId = skill.Id,
};
}).OrderBy(x => x.SkillId).ToList(),
task.Id,
level);
return OperationResult<ProjectSetTimeResponse>.Success(res);
}
private async Task<OperationResult<ProjectSetTimeResponse>> GetPhaseSetTimeDetails(Guid id, ProjectHierarchyLevel level,
CancellationToken cancellationToken)
{
var phase = await _context.ProjectPhases
.Where(p => p.Id == id)
.Include(x => x.PhaseSections).AsNoTracking()
.FirstOrDefaultAsync(cancellationToken);
if (phase == null)
{
return OperationResult<ProjectSetTimeResponse>.NotFound("فاز یافت نشد");
}
var userIds = phase.PhaseSections.Select(x => x.UserId)
.Distinct().ToList();
var users = await _context.Users
.Where(x => userIds.Contains(x.Id))
.AsNoTracking()
.ToListAsync(cancellationToken);
var skills = await _context.Skills
.AsNoTracking()
.ToListAsync(cancellationToken);
var res = new ProjectSetTimeResponse(
skills.Select(skill =>
{
var section = phase.PhaseSections
.FirstOrDefault(x => x.SkillId == skill.Id);
var user = users.FirstOrDefault(x => x.Id == section?.UserId);
return new ProjectSetTimeResponseSkill
{
AdditionalTimes = [],
InitCreationTime = "",
SkillName = skill.Name ?? "",
UserFullName = user?.FullName ?? "",
SectionId = section?.Id ?? Guid.Empty,
InitialDescription = "",
InitialHours = 0,
InitialMinutes = 0,
UserId = section?.UserId ?? 0,
SkillId = skill.Id,
};
}).OrderBy(x => x.SkillId).ToList(),
phase.Id,
level);
return OperationResult<ProjectSetTimeResponse>.Success(res);
}
private async Task<OperationResult<ProjectSetTimeResponse>> GetProjectSetTimeDetails(Guid id, ProjectHierarchyLevel level,
CancellationToken cancellationToken)
{
var project = await _context.Projects
.Where(p => p.Id == id)
.Include(x => x.ProjectSections).AsNoTracking()
.FirstOrDefaultAsync(cancellationToken);
if (project == null)
{
return OperationResult<ProjectSetTimeResponse>.NotFound("پروژه یافت نشد");
}
var userIds = project.ProjectSections.Select(x => x.UserId)
.Distinct().ToList();
var users = await _context.Users
.Where(x => userIds.Contains(x.Id))
.AsNoTracking()
.ToListAsync(cancellationToken);
var skills = await _context.Skills
.AsNoTracking()
.ToListAsync(cancellationToken);
var res = new ProjectSetTimeResponse(
skills.Select(skill =>
{
var section = project.ProjectSections
.FirstOrDefault(x => x.SkillId == skill.Id);
var user = users.FirstOrDefault(x => x.Id == section?.UserId);
return new ProjectSetTimeResponseSkill
{
AdditionalTimes = [],
InitCreationTime = "",
SkillName = skill.Name ?? "",
UserFullName = user?.FullName ?? "",
SectionId = section?.Id ?? Guid.Empty,
InitialDescription = "",
InitialHours = 0,
InitialMinutes = 0,
UserId = section?.UserId ?? 0,
SkillId = skill.Id,
};
}).OrderBy(x => x.SkillId).ToList(),
project.Id,
level);
return OperationResult<ProjectSetTimeResponse>.Success(res);
}

View File

@@ -1,13 +1,18 @@
using FluentValidation;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails;
public class ProjectSetTimeDetailsQueryValidator:AbstractValidator<ProjectSetTimeDetailsQuery>
public class ProjectSetTimeDetailsQueryValidator : AbstractValidator<ProjectSetTimeDetailsQuery>
{
public ProjectSetTimeDetailsQueryValidator()
{
RuleFor(x => x.TaskId)
RuleFor(x => x.Id)
.NotEmpty()
.WithMessage("شناسه پروژه نمی‌تواند خالی باشد.");
.WithMessage("شناسه نمی‌تواند خالی باشد.");
RuleFor(x => x.Level)
.IsInEnum()
.WithMessage("سطح معادل نامعتبر است.");
}
}

View File

@@ -0,0 +1,47 @@
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.DeleteMessage;
public record DeleteMessageCommand(Guid MessageId) : IBaseCommand;
public class DeleteMessageCommandHandler : IBaseCommandHandler<DeleteMessageCommand>
{
private readonly ITaskChatMessageRepository _repository;
private readonly IAuthHelper _authHelper;
public DeleteMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
{
_repository = repository;
_authHelper = authHelper;
}
public async Task<OperationResult> Handle(DeleteMessageCommand request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId()??
throw new UnAuthorizedException("کاربر احراز هویت نشده است");
var message = await _repository.GetByIdAsync(request.MessageId);
if (message == null)
{
return OperationResult.NotFound("پیام یافت نشد");
}
try
{
message.DeleteMessage(currentUserId);
await _repository.UpdateAsync(message);
await _repository.SaveChangesAsync();
// TODO: SignalR notification
return OperationResult.Success();
}
catch (Exception ex)
{
return OperationResult.ValidationError(ex.Message);
}
}
}

View File

@@ -0,0 +1,51 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
using MediatR;
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.EditMessage;
public record EditMessageCommand(
Guid MessageId,
string NewTextContent
) : IBaseCommand;
public class EditMessageCommandHandler : IBaseCommandHandler<EditMessageCommand>
{
private readonly ITaskChatMessageRepository _repository;
private readonly IAuthHelper _authHelper;
public EditMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
{
_repository = repository;
_authHelper = authHelper;
}
public async Task<OperationResult> Handle(EditMessageCommand request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId()??
throw new UnAuthorizedException("کاربر احراز هویت نشده است");
var message = await _repository.GetByIdAsync(request.MessageId);
if (message == null)
{
return OperationResult.NotFound("پیام یافت نشد");
}
try
{
message.EditMessage(request.NewTextContent, currentUserId);
await _repository.UpdateAsync(message);
await _repository.SaveChangesAsync();
// TODO: SignalR notification
return OperationResult.Success();
}
catch (Exception ex)
{
return OperationResult.ValidationError(ex.Message);
}
}
}

View File

@@ -0,0 +1,46 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
using MediatR;
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.PinMessage;
public record PinMessageCommand(Guid MessageId) : IBaseCommand;
public class PinMessageCommandHandler : IBaseCommandHandler<PinMessageCommand>
{
private readonly ITaskChatMessageRepository _repository;
private readonly IAuthHelper _authHelper;
public PinMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
{
_repository = repository;
_authHelper = authHelper;
}
public async Task<OperationResult> Handle(PinMessageCommand request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId()??
throw new UnAuthorizedException("کاربر احراز هویت نشده است");
var message = await _repository.GetByIdAsync(request.MessageId);
if (message == null)
{
return OperationResult.NotFound("پیام یافت نشد");
}
try
{
message.PinMessage(currentUserId);
await _repository.UpdateAsync(message);
await _repository.SaveChangesAsync();
return OperationResult.Success();
}
catch (Exception ex)
{
return OperationResult.ValidationError(ex.Message);
}
}
}

View File

@@ -0,0 +1,212 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
using GozareshgirProgramManager.Application.Services.FileManagement;
using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
using GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
using MediatR;
using Microsoft.AspNetCore.Http;
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.SendMessage;
public record SendMessageCommand(
Guid TaskId,
MessageType MessageType,
string? TextContent,
IFormFile? File,
Guid? ReplyToMessageId
) : IBaseCommand<MessageDto>;
public class SendMessageCommandHandler : IBaseCommandHandler<SendMessageCommand, MessageDto>
{
private readonly ITaskChatMessageRepository _messageRepository;
private readonly IUploadedFileRepository _fileRepository;
private readonly IProjectTaskRepository _taskRepository;
private readonly IFileStorageService _fileStorageService;
private readonly IThumbnailGeneratorService _thumbnailService;
private readonly IAuthHelper _authHelper;
public SendMessageCommandHandler(
ITaskChatMessageRepository messageRepository,
IUploadedFileRepository fileRepository,
IProjectTaskRepository taskRepository,
IFileStorageService fileStorageService,
IThumbnailGeneratorService thumbnailService, IAuthHelper authHelper)
{
_messageRepository = messageRepository;
_fileRepository = fileRepository;
_taskRepository = taskRepository;
_fileStorageService = fileStorageService;
_thumbnailService = thumbnailService;
_authHelper = authHelper;
}
public async Task<OperationResult<MessageDto>> Handle(SendMessageCommand request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId()
?? throw new UnAuthorizedException("کاربر احراز هویت نشده است");
var task = await _taskRepository.GetByIdAsync(request.TaskId, cancellationToken);
if (task == null)
{
return OperationResult<MessageDto>.NotFound("تسک یافت نشد");
}
Guid? uploadedFileId = null;
if (request.File != null)
{
if (request.File.Length == 0)
{
return OperationResult<MessageDto>.ValidationError("فایل خالی است");
}
const long maxFileSize = 100 * 1024 * 1024;
if (request.File.Length > maxFileSize)
{
return OperationResult<MessageDto>.ValidationError("حجم فایل بیش از حد مجاز است (حداکثر 100MB)");
}
var fileType = DetectFileType(request.File.ContentType, Path.GetExtension(request.File.FileName));
var uploadedFile = new UploadedFile(
originalFileName: request.File.FileName,
fileSizeBytes: request.File.Length,
mimeType: request.File.ContentType,
fileType: fileType,
category: FileCategory.TaskChatMessage,
uploadedByUserId: currentUserId,
storageProvider: StorageProvider.LocalFileSystem
);
await _fileRepository.AddAsync(uploadedFile);
await _fileRepository.SaveChangesAsync();
try
{
using var stream = request.File.OpenReadStream();
var uploadResult = await _fileStorageService.UploadAsync(
stream,
uploadedFile.UniqueFileName,
"TaskChatMessage"
);
uploadedFile.CompleteUpload(uploadResult.StoragePath, uploadResult.StorageUrl);
if (fileType == FileType.Image)
{
var dimensions = await _thumbnailService.GetImageDimensionsAsync(uploadResult.StoragePath);
if (dimensions.HasValue)
{
uploadedFile.SetImageDimensions(dimensions.Value.Width, dimensions.Value.Height);
}
var thumbnail = await _thumbnailService
.GenerateImageThumbnailAsync(uploadResult.StoragePath, category: "TaskChatMessage");
if (thumbnail.HasValue)
{
uploadedFile.SetThumbnail(thumbnail.Value.ThumbnailUrl);
}
}
await _fileRepository.UpdateAsync(uploadedFile);
await _fileRepository.SaveChangesAsync();
uploadedFileId = uploadedFile.Id;
}
catch (Exception ex)
{
await _fileRepository.DeleteAsync(uploadedFile);
await _fileRepository.SaveChangesAsync();
return OperationResult<MessageDto>.ValidationError($"خطا در آپلود فایل: {ex.Message}");
}
}
var message = new TaskChatMessage(
taskId: request.TaskId,
senderUserId: currentUserId,
messageType: request.MessageType,
textContent: request.TextContent,
uploadedFileId
);
if (request.ReplyToMessageId.HasValue)
{
message.SetReplyTo(request.ReplyToMessageId.Value);
}
await _messageRepository.AddAsync(message);
await _messageRepository.SaveChangesAsync();
if (uploadedFileId.HasValue)
{
var file = await _fileRepository.GetByIdAsync(uploadedFileId.Value);
if (file != null)
{
file.SetReference("TaskChatMessage", message.Id.ToString());
await _fileRepository.UpdateAsync(file);
await _fileRepository.SaveChangesAsync();
}
}
var dto = new MessageDto
{
Id = message.Id,
TaskId = message.TaskId,
SenderUserId = message.SenderUserId,
SenderName = "کاربر",
MessageType = message.MessageType.ToString(),
TextContent = message.TextContent,
ReplyToMessageId = message.ReplyToMessageId,
IsEdited = message.IsEdited,
IsPinned = message.IsPinned,
CreationDate = message.CreationDate,
IsMine = true
};
if (uploadedFileId.HasValue)
{
var file = await _fileRepository.GetByIdAsync(uploadedFileId.Value);
if (file != null)
{
dto.File = new MessageFileDto
{
Id = file.Id,
FileName = file.OriginalFileName,
FileUrl = file.StorageUrl ?? "",
FileSizeBytes = file.FileSizeBytes,
FileType = file.FileType.ToString(),
ThumbnailUrl = file.ThumbnailUrl,
ImageWidth = file.ImageWidth,
ImageHeight = file.ImageHeight,
DurationSeconds = file.DurationSeconds
};
}
}
return OperationResult<MessageDto>.Success(dto);
}
private FileType DetectFileType(string mimeType, string extension)
{
if (mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
return FileType.Image;
if (mimeType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
return FileType.Video;
if (mimeType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase))
return FileType.Audio;
if (new[] { ".zip", ".rar", ".7z", ".tar", ".gz" }.Contains(extension.ToLower()))
return FileType.Archive;
return FileType.Document;
}
}

View File

@@ -0,0 +1,46 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
using MediatR;
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.UnpinMessage;
public record UnpinMessageCommand(Guid MessageId) : IBaseCommand;
public class UnpinMessageCommandHandler : IBaseCommandHandler<UnpinMessageCommand>
{
private readonly ITaskChatMessageRepository _repository;
private readonly IAuthHelper _authHelper;
public UnpinMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
{
_repository = repository;
_authHelper = authHelper;
}
public async Task<OperationResult> Handle(UnpinMessageCommand request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId()??
throw new UnAuthorizedException("کاربر احراز هویت نشده است");
var message = await _repository.GetByIdAsync(request.MessageId);
if (message == null)
{
return OperationResult.NotFound("پیام یافت نشد");
}
try
{
message.UnpinMessage(currentUserId);
await _repository.UpdateAsync(message);
await _repository.SaveChangesAsync();
return OperationResult.Success();
}
catch (Exception ex)
{
return OperationResult.ValidationError(ex.Message);
}
}
}

View File

@@ -0,0 +1,63 @@
namespace GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
public class SendMessageDto
{
public Guid TaskId { get; set; }
public string MessageType { get; set; } = string.Empty; // "Text", "File", "Image", "Voice", "Video"
public string? TextContent { get; set; }
public Guid? FileId { get; set; }
public Guid? ReplyToMessageId { get; set; }
}
public class MessageDto
{
public Guid Id { get; set; }
public Guid TaskId { get; set; }
public long SenderUserId { get; set; }
public string SenderName { get; set; } = string.Empty;
public string MessageType { get; set; } = string.Empty;
public string? TextContent { get; set; }
public MessageFileDto? File { get; set; }
public Guid? ReplyToMessageId { get; set; }
public MessageDto? ReplyToMessage { get; set; }
public bool IsEdited { get; set; }
public DateTime? EditedDate { get; set; }
public bool IsPinned { get; set; }
public DateTime? PinnedDate { get; set; }
public long? PinnedByUserId { get; set; }
public DateTime CreationDate { get; set; }
public bool IsMine { get; set; }
}
public class MessageFileDto
{
public Guid Id { get; set; }
public string FileName { get; set; } = string.Empty;
public string FileUrl { get; set; } = string.Empty;
public long FileSizeBytes { get; set; }
public string FileType { get; set; } = string.Empty;
public string? ThumbnailUrl { get; set; }
public int? ImageWidth { get; set; }
public int? ImageHeight { get; set; }
public int? DurationSeconds { get; set; }
public string FileSizeFormatted
{
get
{
const long kb = 1024;
const long mb = kb * 1024;
const long gb = mb * 1024;
if (FileSizeBytes >= gb)
return $"{FileSizeBytes / (double)gb:F2} GB";
if (FileSizeBytes >= mb)
return $"{FileSizeBytes / (double)mb:F2} MB";
if (FileSizeBytes >= kb)
return $"{FileSizeBytes / (double)kb:F2} KB";
return $"{FileSizeBytes} Bytes";
}
}
}

View File

@@ -0,0 +1,194 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
using GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetMessages;
public record GetMessagesQuery(
Guid TaskId,
MessageType? MessageType,
int Page = 1,
int PageSize = 50
) : IBaseQuery<PaginationResult<MessageDto>>;
public class GetMessagesQueryHandler : IBaseQueryHandler<GetMessagesQuery, PaginationResult<MessageDto>>
{
private readonly IProgramManagerDbContext _context;
private readonly IAuthHelper _authHelper;
public GetMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper)
{
_context = context;
_authHelper = authHelper;
}
public async Task<OperationResult<PaginationResult<MessageDto>>> Handle(GetMessagesQuery request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId();
var skip = (request.Page - 1) * request.PageSize;
var query = _context.TaskChatMessages
.Where(m => m.TaskId == request.TaskId && !m.IsDeleted)
.Include(m => m.ReplyToMessage)
.OrderBy(m => m.CreationDate).AsQueryable();
if (request.MessageType.HasValue)
{
query = query.Where(m => m.MessageType == request.MessageType.Value);
}
var totalCount = await query.CountAsync(cancellationToken);
var messages = await query
.Skip(skip)
.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 = senderName, // ✅ از User واقعی استفاده می‌کنیم
MessageType = message.MessageType.ToString(),
TextContent = message.TextContent,
ReplyToMessageId = message.ReplyToMessageId,
IsEdited = message.IsEdited,
EditedDate = message.EditedDate,
IsPinned = message.IsPinned,
PinnedDate = message.PinnedDate,
PinnedByUserId = message.PinnedByUserId,
CreationDate = message.CreationDate,
IsMine = message.SenderUserId == currentUserId
};
if (message.ReplyToMessage != null)
{
// ✅ برای پیام‌های 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 = replySenderName,
TextContent = message.ReplyToMessage.TextContent,
CreationDate = message.ReplyToMessage.CreationDate
};
}
if (message.FileId.HasValue)
{
var file = await _context.UploadedFiles.FirstOrDefaultAsync(f => f.Id == message.FileId.Value, cancellationToken);
if (file != null)
{
dto.File = new MessageFileDto
{
Id = file.Id,
FileName = file.OriginalFileName,
FileUrl = file.StorageUrl ?? "",
FileSizeBytes = file.FileSizeBytes,
FileType = file.FileType.ToString(),
ThumbnailUrl = file.ThumbnailUrl,
ImageWidth = file.ImageWidth,
ImageHeight = file.ImageHeight,
DurationSeconds = file.DurationSeconds
};
}
}
messageDtos.Add(dto);
// ✅ اینجا بخش جدید است: نوت‌های زمان اضافی را بین پیام‌ها اضافه کن
// این بخش تمام 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,
TotalCount = totalCount,
};
return OperationResult<PaginationResult<MessageDto>>.Success(response);
}
}

View File

@@ -0,0 +1,82 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetPinnedMessages;
public record GetPinnedMessagesQuery(Guid TaskId) : IBaseQuery<List<MessageDto>>;
public class GetPinnedMessagesQueryHandler : IBaseQueryHandler<GetPinnedMessagesQuery, List<MessageDto>>
{
private readonly IProgramManagerDbContext _context;
private readonly IAuthHelper _authHelper;
public GetPinnedMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper)
{
_context = context;
_authHelper = authHelper;
}
public async Task<OperationResult<List<MessageDto>>> Handle(GetPinnedMessagesQuery request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId();
var messages = await _context.TaskChatMessages
.Where(m => m.TaskId == request.TaskId && m.IsPinned && !m.IsDeleted)
.Include(m => m.ReplyToMessage)
.OrderByDescending(m => m.PinnedDate)
.ToListAsync(cancellationToken);
// ✅ گرفتن تمامی کاربران برای نمایش نام کامل فرستنده
var senderUserIds = messages.Select(m => m.SenderUserId).Distinct().ToList();
var users = await _context.Users
.Where(u => senderUserIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
var messageDtos = new List<MessageDto>();
foreach (var message in messages)
{
// ✅ نام فرستنده را از User واقعی بگیر (به جای "کاربر" ثابت)
var senderName = users.GetValueOrDefault(message.SenderUserId, "کاربر ناشناس");
var dto = new MessageDto
{
Id = message.Id,
TaskId = message.TaskId,
SenderUserId = message.SenderUserId,
SenderName = senderName,
MessageType = message.MessageType.ToString(),
TextContent = message.TextContent,
IsPinned = message.IsPinned,
PinnedDate = message.PinnedDate,
PinnedByUserId = message.PinnedByUserId,
CreationDate = message.CreationDate,
IsMine = message.SenderUserId == currentUserId
};
if (message.FileId.HasValue)
{
var file = await _context.UploadedFiles.FirstOrDefaultAsync(f => f.Id == message.FileId.Value, cancellationToken);
if (file != null)
{
dto.File = new MessageFileDto
{
Id = file.Id,
FileName = file.OriginalFileName,
FileUrl = file.StorageUrl ?? "",
FileSizeBytes = file.FileSizeBytes,
FileType = file.FileType.ToString(),
ThumbnailUrl = file.ThumbnailUrl
};
}
}
messageDtos.Add(dto);
}
return OperationResult<List<MessageDto>>.Success(messageDtos);
}
}

View File

@@ -0,0 +1,72 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.SearchMessages;
public record SearchMessagesQuery(
Guid TaskId,
string SearchText,
int Page = 1,
int PageSize = 20
) : IBaseQuery<List<MessageDto>>;
public class SearchMessagesQueryHandler : IBaseQueryHandler<SearchMessagesQuery, List<MessageDto>>
{
private readonly IProgramManagerDbContext _context;
private readonly IAuthHelper _authHelper;
public SearchMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper)
{
_context = context;
_authHelper = authHelper;
}
public async Task<OperationResult<List<MessageDto>>> Handle(SearchMessagesQuery request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId();
var skip = (request.Page - 1) * request.PageSize;
var messages = await _context.TaskChatMessages
.Where(m => m.TaskId == request.TaskId &&
m.TextContent != null &&
m.TextContent.Contains(request.SearchText) &&
!m.IsDeleted)
.Include(m => m.ReplyToMessage)
.OrderByDescending(m => m.CreationDate)
.Skip(skip)
.Take(request.PageSize)
.ToListAsync(cancellationToken);
// ✅ گرفتن تمامی کاربران برای نمایش نام کامل فرستنده
var senderUserIds = messages.Select(m => m.SenderUserId).Distinct().ToList();
var users = await _context.Users
.Where(u => senderUserIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
var messageDtos = new List<MessageDto>();
foreach (var message in messages)
{
// ✅ نام فرستنده را از User واقعی بگیر
var senderName = users.GetValueOrDefault(message.SenderUserId, "کاربر ناشناس");
var dto = new MessageDto
{
Id = message.Id,
TaskId = message.TaskId,
SenderUserId = message.SenderUserId,
SenderName = senderName,
MessageType = message.MessageType.ToString(),
TextContent = message.TextContent,
CreationDate = message.CreationDate,
IsMine = message.SenderUserId == currentUserId
};
messageDtos.Add(dto);
}
return OperationResult<List<MessageDto>>.Success(messageDtos);
}
}

View File

@@ -0,0 +1,38 @@
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
namespace GozareshgirProgramManager.Application.Services.FileManagement;
/// <summary>
/// سرویس ذخیره‌سازی فایل
/// </summary>
public interface IFileStorageService
{
/// <summary>
/// آپلود فایل
/// </summary>
Task<(string StoragePath, string StorageUrl)> UploadAsync(
Stream fileStream,
string uniqueFileName,
string category);
/// <summary>
/// حذف فایل
/// </summary>
Task DeleteAsync(string storagePath);
/// <summary>
/// دریافت فایل
/// </summary>
Task<Stream?> GetFileStreamAsync(string storagePath);
/// <summary>
/// بررسی وجود فایل
/// </summary>
Task<bool> ExistsAsync(string storagePath);
/// <summary>
/// دریافت URL فایل
/// </summary>
string GetFileUrl(string storagePath);
}

View File

@@ -0,0 +1,34 @@
namespace GozareshgirProgramManager.Application.Services.FileManagement;
/// <summary>
/// سرویس تولید thumbnail برای تصاویر و ویدیوها
/// </summary>
public interface IThumbnailGeneratorService
{
/// <summary>
/// تولید thumbnail برای تصویر
/// </summary>
Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync(
string imagePath,
string category,
int width = 200,
int height = 200);
/// <summary>
/// تولید thumbnail برای ویدیو
/// </summary>
Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(
string videoPath,
string category);
/// <summary>
/// حذف thumbnail
/// </summary>
Task DeleteThumbnailAsync(string thumbnailPath);
/// <summary>
/// دریافت ابعاد تصویر
/// </summary>
Task<(int Width, int Height)?> GetImageDimensionsAsync(string imagePath);
}

View File

@@ -7,6 +7,8 @@ using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities;
using GozareshgirProgramManager.Domain.SkillAgg.Entities;
using GozareshgirProgramManager.Domain.UserAgg.Entities;
using Microsoft.EntityFrameworkCore;
using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
namespace GozareshgirProgramManager.Application._Common.Interfaces;
@@ -26,6 +28,9 @@ public interface IProgramManagerDbContext
DbSet<ProjectTask> ProjectTasks { get; set; }
DbSet<TaskChatMessage> TaskChatMessages { get; set; }
DbSet<UploadedFile> UploadedFiles { get; set; }
DbSet<Skill> Skills { get; set; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,244 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
using GozareshgirProgramManager.Domain.FileManagementAgg.Events;
using FileType = GozareshgirProgramManager.Domain.FileManagementAgg.Enums.FileType;
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
/// <summary>
/// فایل آپلود شده - Aggregate Root
/// مدیریت مرکزی تمام فایل‌های سیستم
/// </summary>
public class UploadedFile : EntityBase<Guid>
{
private UploadedFile()
{
}
public UploadedFile(
string originalFileName,
long fileSizeBytes,
string mimeType,
FileType fileType,
FileCategory category,
long uploadedByUserId,
StorageProvider storageProvider = StorageProvider.LocalFileSystem)
{
OriginalFileName = originalFileName;
FileSizeBytes = fileSizeBytes;
MimeType = mimeType;
FileType = fileType;
Category = category;
UploadedByUserId = uploadedByUserId;
UploadDate = DateTime.Now;
StorageProvider = storageProvider;
Status = FileStatus.Uploading;
// Generate unique file name
FileExtension = Path.GetExtension(originalFileName);
UniqueFileName = $"{Guid.NewGuid()}{FileExtension}";
ValidateFile();
AddDomainEvent(new FileUploadStartedEvent(Id, originalFileName, uploadedByUserId));
}
// اطلاعات فایل
public string OriginalFileName { get; private set; } = string.Empty;
public string UniqueFileName { get; private set; } = string.Empty;
public string FileExtension { get; private set; } = string.Empty;
public long FileSizeBytes { get; private set; }
public string MimeType { get; private set; } = string.Empty;
public FileType FileType { get; private set; }
public FileCategory Category { get; private set; }
// ذخیره‌سازی
public StorageProvider StorageProvider { get; private set; }
public string? StoragePath { get; private set; }
public string? StorageUrl { get; private set; }
public string? ThumbnailUrl { get; private set; }
// متادیتا
public long UploadedByUserId { get; private set; }
public DateTime UploadDate { get; private set; }
public FileStatus Status { get; private set; }
// اطلاعات تصویر (اختیاری - برای Image)
public int? ImageWidth { get; private set; }
public int? ImageHeight { get; private set; }
// اطلاعات صوت/ویدیو (اختیاری)
public int? DurationSeconds { get; private set; }
// امنیت
public DateTime? VirusScanDate { get; private set; }
public bool? IsVirusScanPassed { get; private set; }
public string? VirusScanResult { get; private set; }
// Soft Delete
public bool IsDeleted { get; private set; }
public DateTime? DeletedDate { get; private set; }
public long? DeletedByUserId { get; private set; }
// Reference tracking (چه entityهایی از این فایل استفاده می‌کنند)
public string? ReferenceEntityType { get; private set; }
public string? ReferenceEntityId { get; private set; }
private void ValidateFile()
{
if (string.IsNullOrWhiteSpace(OriginalFileName))
{
throw new BadRequestException("نام فایل نمی‌تواند خالی باشد");
}
if (FileSizeBytes <= 0)
{
throw new BadRequestException("حجم فایل باید بیشتر از صفر باشد");
}
if (string.IsNullOrWhiteSpace(MimeType))
{
throw new BadRequestException("نوع MIME فایل باید مشخص شود");
}
// محدودیت حجم (مثلاً 100MB)
const long maxSizeBytes = 100 * 1024 * 1024; // 100MB
if (FileSizeBytes > maxSizeBytes)
{
throw new BadRequestException($"حجم فایل نباید بیشتر از {maxSizeBytes / (1024 * 1024)} مگابایت باشد");
}
}
public void CompleteUpload(string storagePath, string storageUrl)
{
if (Status != FileStatus.Uploading)
{
throw new BadRequestException("فایل قبلاً آپلود شده است");
}
if (string.IsNullOrWhiteSpace(storagePath))
{
throw new BadRequestException("مسیر ذخیره‌سازی نمی‌تواند خالی باشد");
}
if (string.IsNullOrWhiteSpace(storageUrl))
{
throw new BadRequestException("URL فایل نمی‌تواند خالی باشد");
}
StoragePath = storagePath;
StorageUrl = storageUrl;
Status = FileStatus.Active;
AddDomainEvent(new FileUploadCompletedEvent(Id, OriginalFileName, StorageUrl, UploadedByUserId));
}
public void SetThumbnail(string thumbnailUrl)
{
if (FileType != FileType.Image && FileType != FileType.Video)
{
throw new BadRequestException("فقط می‌توان برای تصاویر و ویدیوها thumbnail تنظیم کرد");
}
ThumbnailUrl = thumbnailUrl;
}
public void SetImageDimensions(int width, int height)
{
if (FileType != FileType.Image)
{
throw new BadRequestException("فقط می‌توان برای تصاویر ابعاد تنظیم کرد");
}
if (width <= 0 || height <= 0)
{
throw new BadRequestException("ابعاد تصویر باید بیشتر از صفر باشد");
}
ImageWidth = width;
ImageHeight = height;
}
public void SetDuration(int durationSeconds)
{
if (FileType != FileType.Audio && FileType != FileType.Video)
{
throw new BadRequestException("فقط می‌توان برای فایل‌های صوتی و تصویری مدت زمان تنظیم کرد");
}
if (durationSeconds <= 0)
{
throw new BadRequestException("مدت زمان باید بیشتر از صفر باشد");
}
DurationSeconds = durationSeconds;
}
public void MarkAsDeleted(long deletedByUserId)
{
if (IsDeleted)
{
throw new BadRequestException("فایل قبلاً حذف شده است");
}
IsDeleted = true;
DeletedDate = DateTime.Now;
DeletedByUserId = deletedByUserId;
Status = FileStatus.Deleted;
AddDomainEvent(new FileDeletedEvent(Id, OriginalFileName, deletedByUserId));
}
public void SetReference(string entityType, string entityId)
{
ReferenceEntityType = entityType;
ReferenceEntityId = entityId;
}
public bool IsImage()
{
return FileType == FileType.Image;
}
public bool IsVideo()
{
return FileType == FileType.Video;
}
public bool IsAudio()
{
return FileType == FileType.Audio;
}
public bool IsDocument()
{
return FileType == FileType.Document;
}
public bool IsUploadedBy(long userId)
{
return UploadedByUserId == userId;
}
public bool IsActive()
{
return Status == FileStatus.Active && !IsDeleted;
}
public string GetFileSizeFormatted()
{
const long kb = 1024;
const long mb = kb * 1024;
const long gb = mb * 1024;
if (FileSizeBytes >= gb)
return $"{FileSizeBytes / (double)gb:F2} GB";
if (FileSizeBytes >= mb)
return $"{FileSizeBytes / (double)mb:F2} MB";
if (FileSizeBytes >= kb)
return $"{FileSizeBytes / (double)kb:F2} KB";
return $"{FileSizeBytes} Bytes";
}
}

View File

@@ -0,0 +1,15 @@
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
/// <summary>
/// دسته‌بندی فایل - مشخص می‌کند فایل در کجا استفاده شده
/// </summary>
public enum FileCategory
{
TaskChatMessage = 1, // پیام چت تسک
TaskAttachment = 2, // ضمیمه تسک
ProjectDocument = 3, // مستندات پروژه
UserProfilePhoto = 4, // عکس پروفایل کاربر
Report = 5, // گزارش
Other = 6 // سایر
}

View File

@@ -0,0 +1,13 @@
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
/// <summary>
/// وضعیت فایل
/// </summary>
public enum FileStatus
{
Uploading = 1, // در حال آپلود
Active = 2, // فعال و قابل استفاده
Deleted = 5, // حذف شده (Soft Delete)
Archived = 6 // آرشیو شده
}

View File

@@ -0,0 +1,15 @@
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
/// <summary>
/// نوع فایل
/// </summary>
public enum FileType
{
Document = 1, // اسناد (PDF, Word, Excel, etc.)
Image = 2, // تصویر
Video = 3, // ویدیو
Audio = 4, // صوت
Archive = 5, // فایل فشرده (ZIP, RAR)
Other = 6 // سایر
}

View File

@@ -0,0 +1,10 @@
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
/// <summary>
/// نوع ذخیره‌ساز فایل
/// </summary>
public enum StorageProvider
{
LocalFileSystem = 1, // دیسک محلی سرور
}

View File

@@ -0,0 +1,36 @@
using GozareshgirProgramManager.Domain._Common;
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Events;
// File Upload Events
public record FileUploadStartedEvent(Guid FileId, string FileName, long UploadedByUserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.Now;
}
public record FileUploadCompletedEvent(Guid FileId, string FileName, string StorageUrl, long UploadedByUserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.Now;
}
public record FileDeletedEvent(Guid FileId, string FileName, long DeletedByUserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.Now;
}
// Virus Scan Events
public record FileQuarantinedEvent(Guid FileId, string FileName) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.Now;
}
public record FileVirusScanPassedEvent(Guid FileId, string FileName) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.Now;
}
public record FileInfectedEvent(Guid FileId, string FileName, string ScanResult) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.Now;
}

View File

@@ -0,0 +1,91 @@
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
namespace GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
/// <summary>
/// Repository برای مدیریت فایل‌های آپلود شده
/// </summary>
public interface IUploadedFileRepository
{
/// <summary>
/// دریافت فایل بر اساس شناسه
/// </summary>
Task<UploadedFile?> GetByIdAsync(Guid fileId);
/// <summary>
/// دریافت فایل بر اساس نام یکتا
/// </summary>
Task<UploadedFile?> GetByUniqueFileNameAsync(string uniqueFileName);
/// <summary>
/// دریافت لیست فایل‌های یک کاربر
/// </summary>
Task<List<UploadedFile>> GetUserFilesAsync(long userId, int pageNumber, int pageSize);
/// <summary>
/// دریافت فایل‌های یک دسته خاص
/// </summary>
Task<List<UploadedFile>> GetByCategoryAsync(FileCategory category, int pageNumber, int pageSize);
/// <summary>
/// دریافت فایل‌های با وضعیت خاص
/// </summary>
Task<List<UploadedFile>> GetByStatusAsync(FileStatus status, int pageNumber, int pageSize);
/// <summary>
/// دریافت فایل‌های یک Reference خاص
/// </summary>
Task<List<UploadedFile>> GetByReferenceAsync(string entityType, string entityId);
/// <summary>
/// جستجو در فایل‌ها بر اساس نام
/// </summary>
Task<List<UploadedFile>> SearchByNameAsync(string searchTerm, int pageNumber, int pageSize);
/// <summary>
/// دریافت تعداد کل فایل‌های یک کاربر
/// </summary>
Task<int> GetUserFilesCountAsync(long userId);
/// <summary>
/// دریافت مجموع حجم فایل‌های یک کاربر (به بایت)
/// </summary>
Task<long> GetUserTotalFileSizeAsync(long userId);
/// <summary>
/// دریافت فایل‌های منقضی شده برای پاک‌سازی
/// </summary>
Task<List<UploadedFile>> GetExpiredFilesAsync(DateTime olderThan);
/// <summary>
/// اضافه کردن فایل جدید
/// </summary>
Task<UploadedFile> AddAsync(UploadedFile file);
/// <summary>
/// به‌روزرسانی فایل
/// </summary>
Task UpdateAsync(UploadedFile file);
/// <summary>
/// حذف فیزیکی فایل (فقط برای cleanup)
/// </summary>
Task DeleteAsync(UploadedFile file);
/// <summary>
/// ذخیره تغییرات
/// </summary>
Task<int> SaveChangesAsync();
/// <summary>
/// بررسی وجود فایل
/// </summary>
Task<bool> ExistsAsync(Guid fileId);
/// <summary>
/// بررسی وجود فایل با نام یکتا
/// </summary>
Task<bool> ExistsByUniqueFileNameAsync(string uniqueFileName);
}

View File

@@ -74,6 +74,15 @@ public class Project : ProjectHierarchyNode
}
}
public void RemoveProjectSection(Guid skillId)
{
var section = _projectSections.FirstOrDefault(s => s.SkillId == skillId);
if (section != null)
{
_projectSections.Remove(section);
}
}
public void ClearProjectSections()
{
_projectSections.Clear();

View File

@@ -87,6 +87,15 @@ public class ProjectPhase : ProjectHierarchyNode
}
}
public void RemovePhaseSection(Guid skillId)
{
var section = _phaseSections.FirstOrDefault(s => s.SkillId == skillId);
if (section != null)
{
_phaseSections.Remove(section);
}
}
public void ClearPhaseSections()
{
_phaseSections.Clear();

View File

@@ -246,4 +246,9 @@ public class ProjectTask : ProjectHierarchyNode
}
#endregion
public void ClearTaskSections()
{
_sections.Clear();
}
}

View File

@@ -13,4 +13,5 @@ public interface ITaskSectionRepository: IRepository<Guid,TaskSection>
Task<List<TaskSection>> GetAssignedToUserAsync(long userId);
Task<List<TaskSection>> GetActiveSectionsIncludeAllAsync(CancellationToken cancellationToken);
Task<bool> HasUserAnyInProgressSectionAsync(long userId, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,183 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.TaskChatAgg.Events;
using MessageType = GozareshgirProgramManager.Domain.TaskChatAgg.Enums.MessageType;
namespace GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
/// <summary>
/// پیام چت تسک - Aggregate Root
/// هر کسی که به تسک دسترسی داشته باشد می‌تواند پیام‌ها را ببیند و ارسال کند
/// نیازی به مدیریت گروه و ممبر نیست چون دسترسی از طریق خود تسک کنترل می‌شود
/// </summary>
public class TaskChatMessage : EntityBase<Guid>
{
private TaskChatMessage()
{
}
public TaskChatMessage(Guid taskId, long senderUserId, MessageType messageType,
string? textContent = null,Guid? fileId = null)
{
TaskId = taskId;
SenderUserId = senderUserId;
MessageType = messageType;
TextContent = textContent;
IsEdited = false;
IsDeleted = false;
IsPinned = false;
if (fileId.HasValue)
{
SetFile(fileId.Value);
}
ValidateMessage();
AddDomainEvent(new TaskChatMessageSentEvent(Id, taskId, senderUserId, messageType));
}
// Reference به Task (Foreign Key فقط - بدون Navigation Property برای جلوگیری از coupling)
public Guid TaskId { get; private set; }
public long SenderUserId { get; private set; }
public MessageType MessageType { get; private set; }
// محتوای متنی (برای پیام‌های Text و Caption برای فایل/تصویر)
public string? TextContent { get; private set; }
// ارجاع به فایل (برای پیام‌های File, Voice, Image, Video)
public Guid? FileId { get; private set; }
// پیام Reply
public Guid? ReplyToMessageId { get; private set; }
public TaskChatMessage? ReplyToMessage { get; private set; }
// وضعیت پیام
public bool IsEdited { get; private set; }
public DateTime? EditedDate { get; private set; }
public bool IsDeleted { get; private set; }
public DateTime? DeletedDate { get; private set; }
public bool IsPinned { get; private set; }
public DateTime? PinnedDate { get; private set; }
public long? PinnedByUserId { get; private set; }
private void ValidateMessage()
{
// ✅ بررسی پیام‌های متنی
if (MessageType == MessageType.Text && string.IsNullOrWhiteSpace(TextContent))
{
throw new BadRequestException("پیام متنی نمی‌تواند خالی باشد");
}
// ✅ بررسی پیام‌های فایلی - باید FileId داشته باشند
if ((MessageType == MessageType.File || MessageType == MessageType.Voice ||
MessageType == MessageType.Image || MessageType == MessageType.Video)
&& FileId == null)
{
throw new BadRequestException("پیام‌های فایلی باید شناسه فایل داشته باشند");
}
// ✅ بررسی یادداشت‌های سیستم - باید محتوای متنی داشته باشند
if (MessageType == MessageType.Note && string.IsNullOrWhiteSpace(TextContent))
{
throw new BadRequestException("یادداشت نمی‌تواند خالی باشد");
}
}
public void SetFile(Guid fileId)
{
if (MessageType != MessageType.File && MessageType != MessageType.Image &&
MessageType != MessageType.Video && MessageType != MessageType.Voice)
{
throw new BadRequestException("فقط می‌توان برای پیام‌های فایل، تصویر، ویدیو و صدا شناسه فایل تنظیم کرد");
}
FileId = fileId;
}
public void EditMessage(string newTextContent, long editorUserId)
{
if (IsDeleted)
{
throw new BadRequestException("نمی‌توان پیام حذف شده را ویرایش کرد");
}
if (editorUserId != SenderUserId)
{
throw new BadRequestException("فقط فرستنده می‌تواند پیام را ویرایش کند");
}
if ((MessageType != MessageType.Text && !string.IsNullOrWhiteSpace(TextContent)))
{
throw new BadRequestException("فقط پیام‌های متنی قابل ویرایش هستند");
}
if (string.IsNullOrWhiteSpace(newTextContent))
{
throw new BadRequestException("محتوای پیام نمی‌تواند خالی باشد");
}
TextContent = newTextContent;
IsEdited = true;
EditedDate = DateTime.Now;
AddDomainEvent(new TaskChatMessageEditedEvent(Id, TaskId, editorUserId));
}
public void DeleteMessage(long deleterUserId)
{
if (IsDeleted)
{
throw new BadRequestException("پیام قبلاً حذف شده است");
}
if (deleterUserId != SenderUserId)
{
throw new BadRequestException("فقط فرستنده می‌تواند پیام را حذف کند");
}
IsDeleted = true;
DeletedDate = DateTime.Now;
AddDomainEvent(new TaskChatMessageDeletedEvent(Id, TaskId, deleterUserId));
}
public void PinMessage(long pinnerUserId)
{
if (IsDeleted)
{
throw new BadRequestException("نمی‌توان پیام حذف شده را پین کرد");
}
if (IsPinned)
{
throw new BadRequestException("این پیام قبلاً پین شده است");
}
IsPinned = true;
PinnedDate = DateTime.Now;
PinnedByUserId = pinnerUserId;
AddDomainEvent(new TaskChatMessagePinnedEvent(Id, TaskId, pinnerUserId));
}
public void UnpinMessage(long unpinnerUserId)
{
if (!IsPinned)
{
throw new BadRequestException("این پیام پین نشده است");
}
IsPinned = false;
PinnedDate = null;
PinnedByUserId = null;
AddDomainEvent(new TaskChatMessageUnpinnedEvent(Id, TaskId, unpinnerUserId));
}
public void SetReplyTo(Guid replyToMessageId)
{
ReplyToMessageId = replyToMessageId;
}
public bool IsSentBy(long userId)
{
return SenderUserId == userId;
}
}

View File

@@ -0,0 +1,15 @@
namespace GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
/// <summary>
/// نوع پیام در چت تسک
/// </summary>
public enum MessageType
{
Text = 1, // پیام متنی
File = 2, // فایل (اسناد، PDF، و غیره)
Image = 3, // تصویر
Voice = 4, // پیام صوتی
Video = 5, // ویدیو
Note = 6, // ✅ یادداشت سیستم (برای زمان اضافی و اطلاعات خودکار)
}

View File

@@ -0,0 +1,31 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
namespace GozareshgirProgramManager.Domain.TaskChatAgg.Events;
// Message Events
public record TaskChatMessageSentEvent(Guid MessageId, Guid TaskId, long SenderUserId, MessageType MessageType) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.Now;
}
public record TaskChatMessageEditedEvent(Guid MessageId, Guid TaskId, long EditorUserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.Now;
}
public record TaskChatMessageDeletedEvent(Guid MessageId, Guid TaskId, long DeleterUserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.Now;
}
public record TaskChatMessagePinnedEvent(Guid MessageId, Guid TaskId, long PinnerUserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.Now;
}
public record TaskChatMessageUnpinnedEvent(Guid MessageId, Guid TaskId, long UnpinnerUserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.Now;
}

View File

@@ -0,0 +1,75 @@
using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
namespace GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
/// <summary>
/// Repository برای مدیریت پیام‌های چت تسک
/// </summary>
public interface ITaskChatMessageRepository
{
/// <summary>
/// دریافت پیام بر اساس شناسه
/// </summary>
Task<TaskChatMessage?> GetByIdAsync(Guid messageId);
/// <summary>
/// دریافت لیست پیام‌های یک تسک (با صفحه‌بندی)
/// </summary>
Task<List<TaskChatMessage>> GetTaskMessagesAsync(Guid taskId, int pageNumber, int pageSize);
/// <summary>
/// دریافت تعداد کل پیام‌های یک تسک
/// </summary>
Task<int> GetTaskMessageCountAsync(Guid taskId);
/// <summary>
/// دریافت پیام‌های پین شده یک تسک
/// </summary>
Task<List<TaskChatMessage>> GetPinnedMessagesAsync(Guid taskId);
/// <summary>
/// دریافت آخرین پیام یک تسک
/// </summary>
Task<TaskChatMessage?> GetLastMessageAsync(Guid taskId);
/// <summary>
/// جستجو در پیام‌های یک تسک
/// </summary>
Task<List<TaskChatMessage>> SearchMessagesAsync(Guid taskId, string searchText, int pageNumber, int pageSize);
/// <summary>
/// دریافت پیام‌های یک کاربر خاص در یک تسک
/// </summary>
Task<List<TaskChatMessage>> GetUserMessagesAsync(Guid taskId, long userId, int pageNumber, int pageSize);
/// <summary>
/// دریافت پیام‌های با فایل (تصویر، ویدیو، فایل و...) - پیام‌هایی که FileId دارند
/// </summary>
Task<List<TaskChatMessage>> GetMediaMessagesAsync(Guid taskId, int pageNumber, int pageSize);
/// <summary>
/// اضافه کردن پیام جدید
/// </summary>
Task<TaskChatMessage> AddAsync(TaskChatMessage message);
/// <summary>
/// به‌روزرسانی پیام
/// </summary>
Task UpdateAsync(TaskChatMessage message);
/// <summary>
/// حذف فیزیکی پیام (در صورت نیاز - معمولاً استفاده نمی‌شود)
/// </summary>
Task DeleteAsync(TaskChatMessage message);
/// <summary>
/// ذخیره تغییرات
/// </summary>
Task<int> SaveChangesAsync();
/// <summary>
/// بررسی وجود پیام
/// </summary>
Task<bool> ExistsAsync(Guid messageId);
}

View File

@@ -4,9 +4,11 @@
using FluentValidation;
using GozareshgirProgramManager.Application._Common.Behaviors;
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application.Services.FileManagement;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.CheckoutAgg.Repositories;
using GozareshgirProgramManager.Domain.CustomerAgg.Repositories;
using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
using GozareshgirProgramManager.Domain.RoleAgg.Repositories;
using GozareshgirProgramManager.Domain.RoleAgg.Repositories;
@@ -14,6 +16,7 @@ using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Repositories;
using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Repositories;
using GozareshgirProgramManager.Domain.SkillAgg.Repositories;
using GozareshgirProgramManager.Domain.SkillAgg.Repositories;
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
using GozareshgirProgramManager.Domain.UserAgg.Repositories;
using GozareshgirProgramManager.Infrastructure.Persistence;
using GozareshgirProgramManager.Infrastructure.Persistence.Context;
@@ -82,6 +85,14 @@ public static class DependencyInjection
services.AddScoped<IUserRefreshTokenRepository, UserRefreshTokenRepository>();
// File Management & Task Chat
services.AddScoped<IUploadedFileRepository, Persistence.Repositories.FileManagement.UploadedFileRepository>();
services.AddScoped<ITaskChatMessageRepository, Persistence.Repositories.TaskChat.TaskChatMessageRepository>();
// File Storage Services
services.AddScoped<IFileStorageService, Services.FileManagement.LocalFileStorageService>();
services.AddScoped<IThumbnailGeneratorService, Services.FileManagement.ThumbnailGeneratorService>();
// JWT Settings
services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));

View File

@@ -16,10 +16,19 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<!--<PackageReference Include="System.Text.Encodings.Web" Version="10.0.0" />-->
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Application\GozareshgirProgramManager.Application\GozareshgirProgramManager.Application.csproj" />
<ProjectReference Include="..\..\Domain\GozareshgirProgramManager.Domain\GozareshgirProgramManager.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions">
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.AspNetCore.Hosting.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Hosting.Abstractions">
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.Extensions.Hosting.Abstractions.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,158 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GozareshgirProgramManager.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class addtaskchatuploadedfile : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TaskChatMessages",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TaskId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SenderUserId = table.Column<long>(type: "bigint", nullable: false),
MessageType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
TextContent = table.Column<string>(type: "nvarchar(4000)", maxLength: 4000, nullable: true),
FileId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
ReplyToMessageId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsEdited = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
EditedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeletedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
IsPinned = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
PinnedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
PinnedByUserId = table.Column<long>(type: "bigint", nullable: true),
CreationDate = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TaskChatMessages", x => x.Id);
table.ForeignKey(
name: "FK_TaskChatMessages_TaskChatMessages_ReplyToMessageId",
column: x => x.ReplyToMessageId,
principalTable: "TaskChatMessages",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "UploadedFiles",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
OriginalFileName = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
UniqueFileName = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
FileExtension = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
FileSizeBytes = table.Column<long>(type: "bigint", nullable: false),
MimeType = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
FileType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Category = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
StorageProvider = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
StoragePath = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
StorageUrl = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
ThumbnailUrl = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
UploadedByUserId = table.Column<long>(type: "bigint", nullable: false),
UploadDate = table.Column<DateTime>(type: "datetime2", nullable: false),
Status = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
ImageWidth = table.Column<int>(type: "int", nullable: true),
ImageHeight = table.Column<int>(type: "int", nullable: true),
DurationSeconds = table.Column<int>(type: "int", nullable: true),
VirusScanDate = table.Column<DateTime>(type: "datetime2", nullable: true),
IsVirusScanPassed = table.Column<bool>(type: "bit", nullable: true),
VirusScanResult = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeletedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
DeletedByUserId = table.Column<long>(type: "bigint", nullable: true),
ReferenceEntityType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
ReferenceEntityId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
CreationDate = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UploadedFiles", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_TaskChatMessages_CreationDate",
table: "TaskChatMessages",
column: "CreationDate");
migrationBuilder.CreateIndex(
name: "IX_TaskChatMessages_FileId",
table: "TaskChatMessages",
column: "FileId");
migrationBuilder.CreateIndex(
name: "IX_TaskChatMessages_IsDeleted",
table: "TaskChatMessages",
column: "IsDeleted");
migrationBuilder.CreateIndex(
name: "IX_TaskChatMessages_ReplyToMessageId",
table: "TaskChatMessages",
column: "ReplyToMessageId");
migrationBuilder.CreateIndex(
name: "IX_TaskChatMessages_SenderUserId",
table: "TaskChatMessages",
column: "SenderUserId");
migrationBuilder.CreateIndex(
name: "IX_TaskChatMessages_TaskId",
table: "TaskChatMessages",
column: "TaskId");
migrationBuilder.CreateIndex(
name: "IX_TaskChatMessages_TaskId_IsPinned",
table: "TaskChatMessages",
columns: new[] { "TaskId", "IsPinned" });
migrationBuilder.CreateIndex(
name: "IX_UploadedFiles_Category",
table: "UploadedFiles",
column: "Category");
migrationBuilder.CreateIndex(
name: "IX_UploadedFiles_IsDeleted",
table: "UploadedFiles",
column: "IsDeleted");
migrationBuilder.CreateIndex(
name: "IX_UploadedFiles_ReferenceEntityType_ReferenceEntityId",
table: "UploadedFiles",
columns: new[] { "ReferenceEntityType", "ReferenceEntityId" });
migrationBuilder.CreateIndex(
name: "IX_UploadedFiles_Status",
table: "UploadedFiles",
column: "Status");
migrationBuilder.CreateIndex(
name: "IX_UploadedFiles_UniqueFileName",
table: "UploadedFiles",
column: "UniqueFileName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_UploadedFiles_UploadedByUserId",
table: "UploadedFiles",
column: "UploadedByUserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TaskChatMessages");
migrationBuilder.DropTable(
name: "UploadedFiles");
}
}
}

View File

@@ -102,6 +102,131 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations
b.ToTable("Customers", (string)null);
});
modelBuilder.Entity("GozareshgirProgramManager.Domain.FileManagementAgg.Entities.UploadedFile", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime2");
b.Property<long?>("DeletedByUserId")
.HasColumnType("bigint");
b.Property<DateTime?>("DeletedDate")
.HasColumnType("datetime2");
b.Property<int?>("DurationSeconds")
.HasColumnType("int");
b.Property<string>("FileExtension")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<long>("FileSizeBytes")
.HasColumnType("bigint");
b.Property<string>("FileType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<int?>("ImageHeight")
.HasColumnType("int");
b.Property<int?>("ImageWidth")
.HasColumnType("int");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool?>("IsVirusScanPassed")
.HasColumnType("bit");
b.Property<string>("MimeType")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("OriginalFileName")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("ReferenceEntityId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("ReferenceEntityType")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("StoragePath")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("StorageProvider")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("StorageUrl")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("ThumbnailUrl")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("UniqueFileName")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("UploadDate")
.HasColumnType("datetime2");
b.Property<long>("UploadedByUserId")
.HasColumnType("bigint");
b.Property<DateTime?>("VirusScanDate")
.HasColumnType("datetime2");
b.Property<string>("VirusScanResult")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("Category");
b.HasIndex("IsDeleted");
b.HasIndex("Status");
b.HasIndex("UniqueFileName")
.IsUnique();
b.HasIndex("UploadedByUserId");
b.HasIndex("ReferenceEntityType", "ReferenceEntityId");
b.ToTable("UploadedFiles", (string)null);
});
modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.PhaseSection", b =>
{
b.Property<Guid>("Id")
@@ -495,6 +620,81 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations
b.ToTable("Skills", (string)null);
});
modelBuilder.Entity("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("DeletedDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("EditedDate")
.HasColumnType("datetime2");
b.Property<Guid?>("FileId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("IsEdited")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("IsPinned")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<string>("MessageType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<long?>("PinnedByUserId")
.HasColumnType("bigint");
b.Property<DateTime?>("PinnedDate")
.HasColumnType("datetime2");
b.Property<Guid?>("ReplyToMessageId")
.HasColumnType("uniqueidentifier");
b.Property<long>("SenderUserId")
.HasColumnType("bigint");
b.Property<Guid>("TaskId")
.HasColumnType("uniqueidentifier");
b.Property<string>("TextContent")
.HasMaxLength(4000)
.HasColumnType("nvarchar(4000)");
b.HasKey("Id");
b.HasIndex("CreationDate");
b.HasIndex("FileId");
b.HasIndex("IsDeleted");
b.HasIndex("ReplyToMessageId");
b.HasIndex("SenderUserId");
b.HasIndex("TaskId");
b.HasIndex("TaskId", "IsPinned");
b.ToTable("TaskChatMessages", (string)null);
});
modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.User", b =>
{
b.Property<long>("Id")
@@ -779,6 +979,16 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations
b.Navigation("WorkingHoursList");
});
modelBuilder.Entity("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", b =>
{
b.HasOne("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", "ReplyToMessage")
.WithMany()
.HasForeignKey("ReplyToMessageId")
.OnDelete(DeleteBehavior.NoAction);
b.Navigation("ReplyToMessage");
});
modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.User", b =>
{
b.OwnsMany("GozareshgirProgramManager.Domain.RoleUserAgg.RoleUser", "RoleUser", b1 =>

View File

@@ -1,14 +1,16 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Domain.CheckoutAgg.Entities;
using GozareshgirProgramManager.Domain.CustomerAgg;
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Domain.CustomerAgg;
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.RoleAgg.Entities;
using GozareshgirProgramManager.Domain.RoleUserAgg;
using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities;
using GozareshgirProgramManager.Domain.UserAgg.Entities;
using GozareshgirProgramManager.Domain.SkillAgg.Entities;
using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Infrastructure.Persistence.Context;
@@ -40,6 +42,13 @@ public class ProgramManagerDbContext : DbContext, IProgramManagerDbContext
public DbSet<Role> Roles { get; set; } = null!;
public DbSet<Skill> Skills { get; set; } = null!;
// File Management
public DbSet<UploadedFile> UploadedFiles { get; set; } = null!;
// Task Chat
public DbSet<TaskChatMessage> TaskChatMessages { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ProgramManagerDbContext).Assembly);

View File

@@ -0,0 +1,87 @@
using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace GozareshgirProgramManager.Infrastructure.Persistence.Mappings;
public class TaskChatMessageMapping : IEntityTypeConfiguration<TaskChatMessage>
{
public void Configure(EntityTypeBuilder<TaskChatMessage> builder)
{
builder.ToTable("TaskChatMessages");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.ValueGeneratedNever();
// Task Reference
builder.Property(x => x.TaskId)
.IsRequired();
builder.HasIndex(x => x.TaskId);
// Sender
builder.Property(x => x.SenderUserId)
.IsRequired();
builder.HasIndex(x => x.SenderUserId);
// Message Type
builder.Property(x => x.MessageType)
.IsRequired()
.HasConversion<string>()
.HasMaxLength(50);
// Content
builder.Property(x => x.TextContent)
.HasMaxLength(4000);
// File Reference
builder.Property(x => x.FileId);
builder.HasIndex(x => x.FileId);
// Reply
builder.Property(x => x.ReplyToMessageId);
builder.HasOne(x => x.ReplyToMessage)
.WithMany()
.HasForeignKey(x => x.ReplyToMessageId)
.OnDelete(DeleteBehavior.NoAction);
// Status
builder.Property(x => x.IsEdited)
.IsRequired()
.HasDefaultValue(false);
builder.Property(x => x.EditedDate);
builder.Property(x => x.IsDeleted)
.IsRequired()
.HasDefaultValue(false);
builder.Property(x => x.DeletedDate);
builder.HasIndex(x => x.IsDeleted);
// Pin
builder.Property(x => x.IsPinned)
.IsRequired()
.HasDefaultValue(false);
builder.Property(x => x.PinnedDate);
builder.Property(x => x.PinnedByUserId);
builder.HasIndex(x => new { x.TaskId, x.IsPinned });
// Audit
builder.Property(x => x.CreationDate)
.IsRequired();
builder.HasIndex(x => x.CreationDate);
// Query Filter - پیام‌های حذف نشده
builder.HasQueryFilter(x => !x.IsDeleted);
}
}

View File

@@ -0,0 +1,121 @@
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace GozareshgirProgramManager.Infrastructure.Persistence.Mappings;
public class UploadedFileMapping : IEntityTypeConfiguration<UploadedFile>
{
public void Configure(EntityTypeBuilder<UploadedFile> builder)
{
builder.ToTable("UploadedFiles");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.ValueGeneratedNever();
// اطلاعات فایل
builder.Property(x => x.OriginalFileName)
.IsRequired()
.HasMaxLength(500);
builder.Property(x => x.UniqueFileName)
.IsRequired()
.HasMaxLength(500);
builder.HasIndex(x => x.UniqueFileName)
.IsUnique();
builder.Property(x => x.FileExtension)
.IsRequired()
.HasMaxLength(50);
builder.Property(x => x.FileSizeBytes)
.IsRequired();
builder.Property(x => x.MimeType)
.IsRequired()
.HasMaxLength(200);
builder.Property(x => x.FileType)
.IsRequired()
.HasConversion<string>()
.HasMaxLength(50);
builder.Property(x => x.Category)
.IsRequired()
.HasConversion<string>()
.HasMaxLength(100);
// ذخیره‌سازی
builder.Property(x => x.StorageProvider)
.IsRequired()
.HasConversion<string>()
.HasMaxLength(50);
builder.Property(x => x.StoragePath)
.HasMaxLength(1000);
builder.Property(x => x.StorageUrl)
.HasMaxLength(1000);
builder.Property(x => x.ThumbnailUrl)
.HasMaxLength(1000);
// متادیتا
builder.Property(x => x.UploadedByUserId)
.IsRequired();
builder.Property(x => x.UploadDate)
.IsRequired();
builder.Property(x => x.Status)
.IsRequired()
.HasConversion<string>()
.HasMaxLength(50);
builder.HasIndex(x => x.Status);
builder.HasIndex(x => x.UploadedByUserId);
builder.HasIndex(x => x.Category);
// اطلاعات تصویر
builder.Property(x => x.ImageWidth);
builder.Property(x => x.ImageHeight);
// اطلاعات صوت/ویدیو
builder.Property(x => x.DurationSeconds);
// امنیت
builder.Property(x => x.VirusScanDate);
builder.Property(x => x.IsVirusScanPassed);
builder.Property(x => x.VirusScanResult)
.HasMaxLength(500);
// Soft Delete
builder.Property(x => x.IsDeleted)
.IsRequired()
.HasDefaultValue(false);
builder.Property(x => x.DeletedDate);
builder.Property(x => x.DeletedByUserId);
builder.HasIndex(x => x.IsDeleted);
// Reference Tracking
builder.Property(x => x.ReferenceEntityType)
.HasMaxLength(100);
builder.Property(x => x.ReferenceEntityId)
.HasMaxLength(100);
builder.HasIndex(x => new { x.ReferenceEntityType, x.ReferenceEntityId });
// Audit
builder.Property(x => x.CreationDate)
.IsRequired();
// Query Filter - فایل‌های حذف نشده
builder.HasQueryFilter(x => !x.IsDeleted);
}
}

View File

@@ -0,0 +1,134 @@
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
using GozareshgirProgramManager.Infrastructure.Persistence.Context;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Infrastructure.Persistence.Repositories.FileManagement;
public class UploadedFileRepository : IUploadedFileRepository
{
private readonly ProgramManagerDbContext _context;
public UploadedFileRepository(ProgramManagerDbContext context)
{
_context = context;
}
public async Task<UploadedFile?> GetByIdAsync(Guid fileId)
{
return await _context.UploadedFiles
.FirstOrDefaultAsync(x => x.Id == fileId);
}
public async Task<UploadedFile?> GetByUniqueFileNameAsync(string uniqueFileName)
{
return await _context.UploadedFiles
.FirstOrDefaultAsync(x => x.UniqueFileName == uniqueFileName);
}
public async Task<List<UploadedFile>> GetUserFilesAsync(long userId, int pageNumber, int pageSize)
{
return await _context.UploadedFiles
.Where(x => x.UploadedByUserId == userId)
.OrderByDescending(x => x.UploadDate)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
public async Task<List<UploadedFile>> GetByCategoryAsync(FileCategory category, int pageNumber, int pageSize)
{
return await _context.UploadedFiles
.Where(x => x.Category == category)
.OrderByDescending(x => x.UploadDate)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
public async Task<List<UploadedFile>> GetByStatusAsync(FileStatus status, int pageNumber, int pageSize)
{
return await _context.UploadedFiles
.Where(x => x.Status == status)
.OrderByDescending(x => x.UploadDate)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
public async Task<List<UploadedFile>> GetByReferenceAsync(string entityType, string entityId)
{
return await _context.UploadedFiles
.Where(x => x.ReferenceEntityType == entityType && x.ReferenceEntityId == entityId)
.OrderByDescending(x => x.UploadDate)
.ToListAsync();
}
public async Task<List<UploadedFile>> SearchByNameAsync(string searchTerm, int pageNumber, int pageSize)
{
return await _context.UploadedFiles
.Where(x => x.OriginalFileName.Contains(searchTerm))
.OrderByDescending(x => x.UploadDate)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
public async Task<int> GetUserFilesCountAsync(long userId)
{
return await _context.UploadedFiles
.CountAsync(x => x.UploadedByUserId == userId);
}
public async Task<long> GetUserTotalFileSizeAsync(long userId)
{
return await _context.UploadedFiles
.Where(x => x.UploadedByUserId == userId)
.SumAsync(x => x.FileSizeBytes);
}
public async Task<List<UploadedFile>> GetExpiredFilesAsync(DateTime olderThan)
{
return await _context.UploadedFiles
.IgnoreQueryFilters() // Include deleted files
.Where(x => x.IsDeleted && x.DeletedDate < olderThan)
.ToListAsync();
}
public async Task<UploadedFile> AddAsync(UploadedFile file)
{
await _context.UploadedFiles.AddAsync(file);
return file;
}
public Task UpdateAsync(UploadedFile file)
{
_context.UploadedFiles.Update(file);
return Task.CompletedTask;
}
public Task DeleteAsync(UploadedFile file)
{
_context.UploadedFiles.Remove(file);
return Task.CompletedTask;
}
public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}
public async Task<bool> ExistsAsync(Guid fileId)
{
return await _context.UploadedFiles
.AnyAsync(x => x.Id == fileId);
}
public async Task<bool> ExistsByUniqueFileNameAsync(string uniqueFileName)
{
return await _context.UploadedFiles
.AnyAsync(x => x.UniqueFileName == uniqueFileName);
}
}

View File

@@ -19,6 +19,7 @@ public class ProjectPhaseRepository : RepositoryBase<Guid, ProjectPhase>, IProje
public Task<ProjectPhase?> GetWithTasksAsync(Guid phaseId)
{
return _context.ProjectPhases
.Include(x=>x.PhaseSections)
.Include(p => p.Tasks)
.ThenInclude(t => t.Sections)
.ThenInclude(s => s.Skill)

View File

@@ -0,0 +1,122 @@
using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
using GozareshgirProgramManager.Infrastructure.Persistence.Context;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Infrastructure.Persistence.Repositories.TaskChat;
public class TaskChatMessageRepository : ITaskChatMessageRepository
{
private readonly ProgramManagerDbContext _context;
public TaskChatMessageRepository(ProgramManagerDbContext context)
{
_context = context;
}
public async Task<TaskChatMessage?> GetByIdAsync(Guid messageId)
{
return await _context.TaskChatMessages
.Include(x => x.ReplyToMessage)
.FirstOrDefaultAsync(x => x.Id == messageId);
}
public async Task<List<TaskChatMessage>> GetTaskMessagesAsync(Guid taskId, int pageNumber, int pageSize)
{
return await _context.TaskChatMessages
.Where(x => x.TaskId == taskId)
.Include(x => x.ReplyToMessage)
.OrderBy(x => x.CreationDate)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
public async Task<int> GetTaskMessageCountAsync(Guid taskId)
{
return await _context.TaskChatMessages
.CountAsync(x => x.TaskId == taskId);
}
public async Task<List<TaskChatMessage>> GetPinnedMessagesAsync(Guid taskId)
{
return await _context.TaskChatMessages
.Where(x => x.TaskId == taskId && x.IsPinned)
.Include(x => x.ReplyToMessage)
.OrderByDescending(x => x.PinnedDate)
.ToListAsync();
}
public async Task<TaskChatMessage?> GetLastMessageAsync(Guid taskId)
{
return await _context.TaskChatMessages
.Where(x => x.TaskId == taskId)
.OrderByDescending(x => x.CreationDate)
.FirstOrDefaultAsync();
}
public async Task<List<TaskChatMessage>> SearchMessagesAsync(Guid taskId, string searchText, int pageNumber, int pageSize)
{
return await _context.TaskChatMessages
.Where(x => x.TaskId == taskId &&
x.TextContent != null &&
x.TextContent.Contains(searchText))
.Include(x => x.ReplyToMessage)
.OrderByDescending(x => x.CreationDate)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
public async Task<List<TaskChatMessage>> GetUserMessagesAsync(Guid taskId, long userId, int pageNumber, int pageSize)
{
return await _context.TaskChatMessages
.Where(x => x.TaskId == taskId && x.SenderUserId == userId)
.Include(x => x.ReplyToMessage)
.OrderByDescending(x => x.CreationDate)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
public async Task<List<TaskChatMessage>> GetMediaMessagesAsync(Guid taskId, int pageNumber, int pageSize)
{
return await _context.TaskChatMessages
.Where(x => x.TaskId == taskId && x.FileId != null)
.Include(x => x.ReplyToMessage)
.OrderByDescending(x => x.CreationDate)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
public async Task<TaskChatMessage> AddAsync(TaskChatMessage message)
{
await _context.TaskChatMessages.AddAsync(message);
return message;
}
public Task UpdateAsync(TaskChatMessage message)
{
_context.TaskChatMessages.Update(message);
return Task.CompletedTask;
}
public Task DeleteAsync(TaskChatMessage message)
{
_context.TaskChatMessages.Remove(message);
return Task.CompletedTask;
}
public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}
public async Task<bool> ExistsAsync(Guid messageId)
{
return await _context.TaskChatMessages
.AnyAsync(x => x.Id == messageId);
}
}

View File

@@ -45,4 +45,11 @@ public class TaskSectionRepository:RepositoryBase<Guid,TaskSection>,ITaskSection
.Include(x => x.AdditionalTimes)
.ToListAsync(cancellationToken);
}
public async Task<bool> HasUserAnyInProgressSectionAsync(long userId, CancellationToken cancellationToken = default)
{
return await _context.TaskSections
.AnyAsync(x => x.CurrentAssignedUserId == userId && x.Status == TaskSectionStatus.InProgress,
cancellationToken);
}
}

View File

@@ -0,0 +1,107 @@
using GozareshgirProgramManager.Application.Services.FileManagement;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement;
/// <summary>
/// سرویس ذخیره‌سازی فایل در سیستم فایل محلی
/// </summary>
public class LocalFileStorageService : IFileStorageService
{
private readonly string _uploadBasePath;
private readonly string _baseUrl;
public LocalFileStorageService(IConfiguration configuration,
IHttpContextAccessor httpContextAccessor, IHostEnvironment env)
{
// محاسبه مسیر پایه: اگر env نبود، از مسیر فعلی استفاده کن
var contentRoot = env.ContentRootPath;
_uploadBasePath = Path.Combine(contentRoot, "Storage");
// Base URL برای دسترسی به فایل‌ها
var request = httpContextAccessor.HttpContext?.Request;
if (request != null)
{
_baseUrl = $"{request.Scheme}://{request.Host}/storage";
}
else
{
_baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/storage";
}
// ایجاد پوشه اگر وجود نداشت
if (!Directory.Exists(_uploadBasePath))
{
Directory.CreateDirectory(_uploadBasePath);
}
}
public async Task<(string StoragePath, string StorageUrl)> UploadAsync(
Stream fileStream,
string uniqueFileName,
string category)
{
// ایجاد پوشه دسته‌بندی (مثلاً: Uploads/TaskChatMessage)
var categoryPath = Path.Combine(_uploadBasePath, category);
if (!Directory.Exists(categoryPath))
{
Directory.CreateDirectory(categoryPath);
}
// ایجاد زیرپوشه بر اساس تاریخ (مثلاً: Uploads/TaskChatMessage/2026/01)
var datePath = Path.Combine(categoryPath, DateTime.Now.Year.ToString(),
DateTime.Now.Month.ToString("00"));
if (!Directory.Exists(datePath))
{
Directory.CreateDirectory(datePath);
}
// مسیر کامل فایل
var storagePath = Path.Combine(datePath, uniqueFileName);
// ذخیره فایل
await using var fileStreamOutput = new FileStream(storagePath, FileMode.Create, FileAccess.Write);
await fileStream.CopyToAsync(fileStreamOutput);
// URL فایل
var relativePath = Path.GetRelativePath(_uploadBasePath, storagePath)
.Replace("\\", "/");
var storageUrl = $"{_baseUrl}/{relativePath}";
return (storagePath, storageUrl);
}
public Task DeleteAsync(string storagePath)
{
if (File.Exists(storagePath))
{
File.Delete(storagePath);
}
return Task.CompletedTask;
}
public Task<Stream?> GetFileStreamAsync(string storagePath)
{
if (!File.Exists(storagePath))
{
return Task.FromResult<Stream?>(null);
}
var stream = new FileStream(storagePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return Task.FromResult<Stream?>(stream);
}
public Task<bool> ExistsAsync(string storagePath)
{
return Task.FromResult(File.Exists(storagePath));
}
public string GetFileUrl(string storagePath)
{
var relativePath = Path.GetRelativePath(_uploadBasePath, storagePath)
.Replace("\\", "/");
return $"{_baseUrl}/{relativePath}";
}
}

View File

@@ -0,0 +1,144 @@
using GozareshgirProgramManager.Application.Services.FileManagement;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Formats.Jpeg;
namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement;
/// <summary>
/// سرویس تولید thumbnail با استفاده از ImageSharp
/// </summary>
public class ThumbnailGeneratorService : IThumbnailGeneratorService
{
private readonly string _thumbnailBasePath;
private readonly string _baseUrl;
public ThumbnailGeneratorService(IConfiguration configuration,
IHttpContextAccessor httpContextAccessor)
{
_thumbnailBasePath = configuration["FileStorage:ThumbnailPath"]
?? Path.Combine(Directory.GetCurrentDirectory(), "storage", "Thumbnails");
var request = httpContextAccessor.HttpContext?.Request;
if (request != null)
{
_baseUrl = $"{request.Scheme}://{request.Host}/storage";
}
else
{
_baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/storage";
}
if (!Directory.Exists(_thumbnailBasePath))
{
Directory.CreateDirectory(_thumbnailBasePath);
}
}
public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync(
string imagePath,
string category,
int width = 200,
int height = 200)
{
try
{
if (!File.Exists(imagePath))
{
return null;
}
// بارگذاری تصویر
using var image = await Image.LoadAsync(imagePath);
// Resize با حفظ نسبت
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(width, height),
Mode = ResizeMode.Max
}));
// نام فایل thumbnail
var fileName = Path.GetFileNameWithoutExtension(imagePath);
var extension = Path.GetExtension(imagePath);
var thumbnailFileName = $"{fileName}_thumb{extension}";
// دریافت مسیر و URL توسط متد private
var (thumbnailPath, thumbnailUrl) = GetThumbnailPathAndUrl(thumbnailFileName, category);
// ذخیره thumbnail با کیفیت 80
await image.SaveAsync(thumbnailPath, new JpegEncoder { Quality = 80 });
return (thumbnailPath, thumbnailUrl);
}
catch (Exception)
{
// در صورت خطا null برمی‌گردانیم
return null;
}
}
public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(string videoPath, string category = "general")
{
// TODO: برای Video thumbnail باید از FFmpeg استفاده کنیم
// فعلاً یک placeholder image برمی‌گردانیم
await Task.CompletedTask;
return null;
}
public Task DeleteThumbnailAsync(string thumbnailPath)
{
if (File.Exists(thumbnailPath))
{
File.Delete(thumbnailPath);
}
return Task.CompletedTask;
}
public async Task<(int Width, int Height)?> GetImageDimensionsAsync(string imagePath)
{
try
{
if (!File.Exists(imagePath))
{
return null;
}
var imageInfo = await Image.IdentifyAsync(imagePath);
return (imageInfo.Width, imageInfo.Height);
}
catch (Exception)
{
return null;
}
}
/// <summary>
/// دریافت مسیر فیزیکی و URL برای thumbnail بر اساس category
/// </summary>
private (string ThumbnailPath, string ThumbnailUrl) GetThumbnailPathAndUrl(string thumbnailFileName, string category)
{
var categoryFolder = string.IsNullOrWhiteSpace(category) ? "general" : category;
var categoryPath = Path.Combine(Directory.GetCurrentDirectory(), "storage", categoryFolder);
if (!Directory.Exists(categoryPath))
{
Directory.CreateDirectory(categoryPath);
}
var thumbnailSubPath = Path.Combine(categoryPath, "Thumbnails");
if (!Directory.Exists(thumbnailSubPath))
{
Directory.CreateDirectory(thumbnailSubPath);
}
var thumbnailPath = Path.Combine(thumbnailSubPath, thumbnailFileName);
var thumbnailUrl = $"{_baseUrl}/{categoryFolder}/thumbnails/{thumbnailFileName}";
return (thumbnailPath, thumbnailUrl);
}
}

View File

@@ -1,5 +1,6 @@
using System.Runtime.InteropServices;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.ApproveTaskSectionCompletion;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AssignProject;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoStopOverTimeTaskSections;
using GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoUpdateDeployStatus;
@@ -17,6 +18,7 @@ using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectBoar
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardDetail;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectDeployBoardList;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectHierarchySearch;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ServiceHost.BaseControllers;
@@ -40,6 +42,15 @@ public class ProjectController : ProgramManagerBaseController
return res;
}
[HttpGet("search")]
public async Task<ActionResult<OperationResult<GetProjectSearchResponse>>> Search(
[FromQuery] string search)
{
var searchQuery = new GetProjectSearchQuery(search);
var res = await _mediator.Send(searchQuery);
return res;
}
[HttpPost]
public async Task<ActionResult<OperationResult>> Create([FromBody] CreateProjectCommand command)
{
@@ -147,4 +158,11 @@ public class ProjectController : ProgramManagerBaseController
var res = await _mediator.Send(command);
return res;
}
[HttpPost("approve-completion")]
public async Task<ActionResult<OperationResult>> ApproveTaskSectionCompletion([FromBody] ApproveTaskSectionCompletionCommand command)
{
var res = await _mediator.Send(command);
return res;
}
}

View File

@@ -0,0 +1,147 @@
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.DeleteMessage;
using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.EditMessage;
using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.PinMessage;
using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.SendMessage;
using GozareshgirProgramManager.Application.Modules.TaskChat.Commands.UnpinMessage;
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetMessages;
using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetPinnedMessages;
using GozareshgirProgramManager.Application.Modules.TaskChat.Queries.SearchMessages;
using GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ServiceHost.BaseControllers;
namespace ServiceHost.Areas.Admin.Controllers.ProgramManager;
/// <summary>
/// کنترلر مدیریت چت تسک
/// </summary>
public class TaskChatController : ProgramManagerBaseController
{
private readonly IMediator _mediator;
public TaskChatController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
/// دریافت لیست پیام‌های یک تسک
/// </summary>
/// <param name="taskId">شناسه تسک</param>
/// <param name="messageType">نوع پیام</param>
/// <param name="page">صفحه (پیش‌فرض: 1)</param>
/// <param name="pageSize">تعداد در هر صفحه (پیش‌فرض: 50)</param>
[HttpGet("{taskId:guid}/messages")]
public async Task<ActionResult<OperationResult<PaginationResult<MessageDto>>>> GetMessages(
Guid taskId,
[FromQuery] MessageType? messageType,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 50)
{
var query = new GetMessagesQuery(taskId,messageType, page, pageSize);
var result = await _mediator.Send(query);
return result;
}
/// <summary>
/// دریافت پیام‌های پین شده یک تسک
/// </summary>
/// <param name="taskId">شناسه تسک</param>
[HttpGet("{taskId:guid}/messages/pinned")]
public async Task<ActionResult<OperationResult<List<MessageDto>>>> GetPinnedMessages(Guid taskId)
{
var query = new GetPinnedMessagesQuery(taskId);
var result = await _mediator.Send(query);
return result;
}
/// <summary>
/// جستجو در پیام‌های یک تسک
/// </summary>
/// <param name="taskId">شناسه تسک</param>
/// <param name="search">متن جستجو</param>
/// <param name="page">صفحه</param>
/// <param name="pageSize">تعداد در هر صفحه</param>
[HttpGet("{taskId:guid}/messages/search")]
public async Task<ActionResult<OperationResult<List<MessageDto>>>> SearchMessages(
Guid taskId,
[FromQuery] string search,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var query = new SearchMessagesQuery(taskId, search, page, pageSize);
var result = await _mediator.Send(query);
return result;
}
/// <summary>
/// ارسال پیام جدید (با یا بدون فایل)
/// </summary>
[HttpPost("messages")]
public async Task<ActionResult<OperationResult<MessageDto>>> SendMessage(
[FromForm] SendMessageCommand command)
{
var result = await _mediator.Send(command);
return result;
}
/// <summary>
/// ویرایش پیام (فقط متن)
/// </summary>
/// <param name="messageId">شناسه پیام</param>
/// <param name="request">محتوای جدید</param>
[HttpPut("messages/{messageId:guid}")]
public async Task<ActionResult<OperationResult>> EditMessage(
Guid messageId,
[FromBody] EditMessageRequest request)
{
var command = new EditMessageCommand(messageId, request.NewTextContent);
var result = await _mediator.Send(command);
return result;
}
/// <summary>
/// حذف پیام
/// </summary>
/// <param name="messageId">شناسه پیام</param>
[HttpDelete("messages/{messageId:guid}")]
public async Task<ActionResult<OperationResult>> DeleteMessage(Guid messageId)
{
var command = new DeleteMessageCommand(messageId);
var result = await _mediator.Send(command);
return result;
}
/// <summary>
/// پین کردن پیام
/// </summary>
/// <param name="messageId">شناسه پیام</param>
[HttpPost("messages/{messageId:guid}/pin")]
public async Task<ActionResult<OperationResult>> PinMessage(Guid messageId)
{
var command = new PinMessageCommand(messageId);
var result = await _mediator.Send(command);
return result;
}
/// <summary>
/// برداشتن پین پیام
/// </summary>
/// <param name="messageId">شناسه پیام</param>
[HttpPost("messages/{messageId:guid}/unpin")]
public async Task<ActionResult<OperationResult>> UnpinMessage(Guid messageId)
{
var command = new UnpinMessageCommand(messageId);
var result = await _mediator.Send(command);
return result;
}
}
public class EditMessageRequest
{
public string NewTextContent { get; set; } = string.Empty;
}

View File

@@ -48,7 +48,8 @@ public class institutionContractController : AdminBaseController
IPersonalContractingPartyApp contractingPartyApplication, IContactInfoApplication contactInfoApplication,
IAccountApplication accountApplication, IEmployerApplication employerApplication,
IWorkshopApplication workshopApplication, ITemporaryClientRegistrationApplication temporaryClientRegistration,
ITemporaryClientRegistrationApplication clientRegistrationApplication, IHttpClientFactory httpClientFactory)
ITemporaryClientRegistrationApplication clientRegistrationApplication, IHttpClientFactory httpClientFactory
,ILogger<SepehrPaymentGateway> sepehrGatewayLogger)
{
_institutionContractApplication = institutionContractApplication;
_contractingPartyApplication = contractingPartyApplication;
@@ -58,7 +59,7 @@ public class institutionContractController : AdminBaseController
_workshopApplication = workshopApplication;
_temporaryClientRegistration = temporaryClientRegistration;
_clientRegistrationApplication = clientRegistrationApplication;
_paymentGateway = new SepehrPaymentGateway(httpClientFactory);
_paymentGateway = new SepehrPaymentGateway(httpClientFactory, sepehrGatewayLogger);
}
/// <summary>

View File

@@ -79,7 +79,8 @@ namespace ServiceHost.Areas.AdminNew.Pages.Company.AndroidApk
CompanyContext context, AccountContext accountContext, IHttpClientFactory httpClientFactory,
IOptions<AppSettingConfiguration> appSetting,
ITemporaryClientRegistrationApplication clientRegistrationApplication, IOnlinePayment onlinePayment,
IFaceEmbeddingService faceEmbeddingService, IAuthHelper authHelper, IInstitutionContractApplication institutionContractApplication)
IFaceEmbeddingService faceEmbeddingService, IAuthHelper authHelper, IInstitutionContractApplication institutionContractApplication
,ILogger<SepehrPaymentGateway> sepehrGatewayLogger)
{
_application = application;
_rollCallDomainService = rollCallDomainService;
@@ -91,7 +92,7 @@ namespace ServiceHost.Areas.AdminNew.Pages.Company.AndroidApk
_faceEmbeddingService = faceEmbeddingService;
_authHelper = authHelper;
_institutionContractApplication = institutionContractApplication;
_paymentGateway = new SepehrPaymentGateway(httpClientFactory);
_paymentGateway = new SepehrPaymentGateway(httpClientFactory, sepehrGatewayLogger);
}
public void OnGet()

View File

@@ -219,7 +219,7 @@
</div>
<div class="Rtable-cell--content">
<div class="d-flex align-items-center justify-content-end">
<a href="@Url.Page("./Index", "DownloadFile", new { path = @item.FullPath, fileName = @item.FileName })" class="btn-delete1">
<a href="@Url.Page("./Index", "DownloadFileLog", new { path = @item.FullPath, fileName = @item.FileName })" class="btn-delete1">
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>

View File

@@ -1,4 +1,4 @@
using _0_Framework.Application;
using _0_Framework.Application;
using _0_Framework.Infrastructure;
using _0_Framework.InfraStructure;
using backService;
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Configuration.UserSecrets;
using System.IO;
namespace ServiceHost.Areas.AdminNew.Pages.Company.FileBackup
{
@@ -68,8 +69,8 @@ namespace ServiceHost.Areas.AdminNew.Pages.Company.FileBackup
{
FileName = Path.GetFileName(x),
FullPath = x,
CreationDate = Path.GetFileName(x).ExtractTimeFromInsurancebackup(),
}).OrderByDescending(x => x.CreationDate).ToList();
CreationDate = new DateTime(),
}).ToList();
#endregion
@@ -89,5 +90,22 @@ namespace ServiceHost.Areas.AdminNew.Pages.Company.FileBackup
byte[] fileContent = System.IO.File.ReadAllBytes(path);
return File(fileContent, "application/zip", fileName);
}
}
public IActionResult OnGetDownloadFileLog(string path, string fileName)
{
if (!System.IO.File.Exists(path))
return NotFound();
var stream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite // 🔑 کلید حل مشکل
);
return File(stream, "text/plain", fileName);
}
}
}

View File

@@ -80,7 +80,7 @@ namespace ServiceHost.Areas.AdminNew.Pages.Company.Ticket
public IActionResult OnGetShowDetailTicketByAdmin(long ticketID)
{
var res = _ticketApplication.GetDetails(ticketID);
res.WorkshopName = _workshopApplication.GetDetails(res.WorkshopId).WorkshopFullName;
res.WorkshopName = _workshopApplication.GetDetails(res.WorkshopId)?.WorkshopFullName??"";
return Partial("DetailTicketModal", res);
}

View File

@@ -32,6 +32,8 @@ using GozareshgirProgramManager.Application.Modules.Users.Commands.CreateUser;
using GozareshgirProgramManager.Infrastructure;
using GozareshgirProgramManager.Infrastructure.Persistence.Seed;
using Microsoft.OpenApi;
using Serilog;
using Serilog.Events;
using ServiceHost.Hubs.ProgramManager;
using ServiceHost.Notifications.ProgramManager;
using ServiceHost.Conventions;
@@ -53,7 +55,30 @@ builder.Services.AddHttpClient("holidayApi", c => c.BaseAddress = new System.Uri
var connectionString = builder.Configuration.GetConnectionString("MesbahDb");
var connectionStringTestDb = builder.Configuration.GetConnectionString("TestDb");
#region Serilog
var logDirectory = @"C:\Logs\Gozareshgir\";
if (!Directory.Exists(logDirectory))
{
Directory.CreateDirectory(logDirectory);
}
// فقط برای فایل از Serilog استفاده می‌شود
// تنظیمات MinimumLevel از appsettings.json خوانده می‌شود
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.File(
path: Path.Combine(logDirectory, "gozareshgir_log.txt"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
shared: true,
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}"
).CreateLogger();
#endregion
builder.Services.AddProgramManagerApplication();
builder.Services.AddProgramManagerInfrastructure(builder.Configuration);
@@ -348,7 +373,31 @@ builder.Services.AddParbad().ConfigureGateways(gateways =>
});
// فقط Serilog برای File استفاده می‌شه، کنسول از لاگر پیش‌فرض ASP.NET استفاده می‌کنه
builder.Host.UseSerilog((context, services, configuration) =>
{
var logConfig = 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"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
shared: true,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}"
);
}, writeToProviders: true); // این باعث میشه کنسول پیش‌فرض هم کار کنه
Log.Information("SERILOG STARTED SUCCESSFULLY");
var app = builder.Build();
app.UseCors("AllowSpecificOrigins");
@@ -443,6 +492,24 @@ app.UseHttpsRedirection();
app.UseStaticFiles();
// Static files برای فایل‌های آپلود شده
var uploadsPath = builder.Configuration["FileStorage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "Storage");
if (!Directory.Exists(uploadsPath))
{
Directory.CreateDirectory(uploadsPath);
}
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(uploadsPath),
RequestPath = "/storage",
OnPrepareResponse = ctx =>
{
// Cache برای فایل‌ها (30 روز)
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000");
}
});
app.UseCookiePolicy();

View File

@@ -19,7 +19,7 @@
"sqlDebugging": true,
"dotnetRunMessages": "true",
"nativeDebugging": true,
"applicationUrl": "https://localhost:5004;http://localhost:5003;",
"applicationUrl": "https://localhost:5004;http://localhost:5003;https://192.168.0.117:5006",
"jsWebView2Debugging": false,
"hotReloadEnabled": true
},
@@ -44,7 +44,7 @@
"sqlDebugging": true,
"dotnetRunMessages": "true",
"nativeDebugging": true,
"applicationUrl": "https://localhost:5004;http://localhost:5003;",
"applicationUrl": "https://localhost:5004;http://localhost:5003;https://192.168.0.117:5006;",
"jsWebView2Debugging": false,
"hotReloadEnabled": true
}

View File

@@ -94,11 +94,16 @@
<PackageReference Include="MongoDB.Driver" Version="3.5.2" />
<PackageReference Include="Parbad.AspNetCore" Version="1.5.0" />
<PackageReference Include="Parbad.Storage.Cache" Version="1.5.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="SocialExplorer.FastDBF" Version="1.0.0" />
<PackageReference Include="System.Data.OleDb" Version="10.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Storage\Task\" />

Some files were not shown because too many files have changed in this diff Show More