Compare commits

..

124 Commits

Author SHA1 Message Date
77b5c8168e add project details to CreateTimeRequestDetailsResponse and handle section validation 2026-02-03 15:49:47 +03:30
cd2c770a9f add parameter in get 2026-02-03 15:17:39 +03:30
64693b2ca3 add workflow result 2026-01-25 12:34:27 +03:30
03657b6848 add missing data for task revisions 2026-01-24 17:29:08 +03:30
15f1c938f7 add workflow controller 2026-01-24 14:02:18 +03:30
7e563a0f01 add Task Revision query and revision and request mapping 2026-01-24 11:12:25 +03:30
a3fd3e6920 add mapping 2026-01-22 10:17:04 +03:30
025c59e695 Complete TaskSectionRevisionMapping.cs 2026-01-21 19:21:26 +03:30
36ccd96352 add time section time request mapping 2026-01-21 19:17:52 +03:30
a7c97b22b4 add DependencyInjection for task section revision and tasksection time request 2026-01-21 18:22:27 +03:30
4c143d6bbc add task section revision command 2026-01-21 18:05:14 +03:30
0e5a0a16ac add task section revision and folderize the project domain 2026-01-21 14:12:06 +03:30
88f54b6310 add acceptTimeRequestCommandHandler- NotFinished 2026-01-21 10:28:38 +03:30
d4694e7e1c add Accept time request command 2026-01-20 14:22:09 +03:30
4bde4ade2d add time request status 2026-01-20 11:01:58 +03:30
bd12ff0506 add TaskSectionTimeRequest to programmanager 2026-01-19 15:19:58 +03:30
gozareshgir
8ec13ffae1 Merge branch 'master' of https://pm.gozareshgir.ir/gozareshgir/OriginalGozareshgir 2026-01-14 14:40:50 +03:30
gozareshgir
5508d4e88f Checkout Compute Minuts Base 2026-01-14 14:39:51 +03:30
43abb74c61 Merge branch 'Feature/program-manager/chat'
# Conflicts:
#	ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetMessages/GetMessagesQuery.cs
2026-01-14 10:57:07 +03:30
73e6681baa add message type to search query 2026-01-14 10:46:44 +03:30
90b2fd2eab add order for skills in set time 2026-01-14 10:13:57 +03:30
d9c431e20e add project name search for board list 2026-01-13 09:23:53 +03:30
gozareshgir
2746bf69ea Merge branch 'master' into Feature/SmsRepoetApi 2026-01-12 14:38:15 +03:30
gozareshgir
77dbb50512 BlueDeActiveAfterZeroDebt hangfire completed 2026-01-12 14:32:50 +03:30
gozareshgir
1c7e8824c7 DeActiveInstitutionEndOfContract hangfire completed 2026-01-12 13:10:58 +03:30
0eff1b9a66 change default task priority from medium to low 2026-01-12 12:07:43 +03:30
gozareshgir
0d33d79620 unblock hangfire completed 2026-01-11 22:07:58 +03:30
gozareshgir
e4355faffc block and unblock 2026-01-11 21:10:29 +03:30
gozareshgir
577fe5db76 Merge branch 'master' of https://pm.gozareshgir.ir/gozareshgir/OriginalGozareshgir into Feature/SmsRepoetApi 2026-01-11 12:58:36 +03:30
587fa40d81 fix percnetage condition 2026-01-10 10:45:38 +03:30
b741ab9ed2 fix contains no element error for empty skills 2026-01-10 10:34:20 +03:30
b6fde4903a Merge branch 'Feature/institution-contract/sent-to-customer-flag' 2026-01-08 15:03:07 +03:30
0772604432 feat: enhance GetMessagesQuery to include additional time notes in message retrieval 2026-01-08 15:02:43 +03:30
SamSys
ec8333c715 merg from master 2026-01-08 15:00:40 +03:30
SamSys
8aa93e089a legal Action Sms completed 2026-01-08 14:56:33 +03:30
59891d1199 Merge remote-tracking branch 'origin/master' 2026-01-08 14:16:22 +03:30
7cb39b1b92 feat: add UserId filter to ProjectBoardListQuery for enhanced task assignment tracking 2026-01-08 14:16:08 +03:30
SamSys
5580d56874 change logger on program.cs 2026-01-08 14:14:27 +03:30
SamSys
423b49e6e7 Merge branch 'master' of https://github.com/samsyntax24/OriginalGozareshgir 2026-01-08 14:05:30 +03:30
SamSys
0ab3052251 Send Warning and leagal action Message 2026-01-08 14:04:34 +03:30
38027352d6 Merge branch 'Feature/program-manager/priority'
# Conflicts:
#	ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListResponse.cs
2026-01-08 14:00:49 +03:30
43562fb49c Merge branch 'Feature/program-manager/chat'
# Conflicts:
#	.gitignore
#	ServiceHost/appsettings.Development.json
#	ServiceHost/appsettings.json
2026-01-08 13:51:06 +03:30
SamSys
5202779d9f changes 2026-01-08 12:18:01 +03:30
80a58f8cdc feat: add TaskId to ProjectBoardListQueryHandler and ProjectBoardListResponse for enhanced task tracking 2026-01-08 12:14:26 +03:30
7c611825a4 feat: refactor task priority handling to use ProjectTaskPriority across commands, DTOs, and repositories 2026-01-08 12:09:18 +03:30
SamSys
67a85735f0 merge from master 2026-01-08 11:36:18 +03:30
SamSys
bf46dfd1dc Merge branch 'master' of https://github.com/samsyntax24/OriginalGozareshgir 2026-01-08 11:35:10 +03:30
SamSys
a1ed3ad648 logeer change 2026-01-08 11:35:03 +03:30
SamSys
35e6355069 get Warning sms List on new repo 2026-01-08 11:19:25 +03:30
8679abb1e7 feat: enhance ChangeTaskPriorityCommand to support multi-level priority updates for tasks, phases, and projects 2026-01-08 11:18:15 +03:30
c8dddabdff fix: correct logic for editing text messages in TaskChatMessage.cs 2026-01-08 11:05:36 +03:30
6f076bdc77 feat: update application URL in launchSettings and enhance task sorting by priority in ProjectBoardListQueryHandler 2026-01-08 10:46:04 +03:30
SamSys
4de2e12ac5 AmaApiReport 2026-01-07 18:38:12 +03:30
380ed8f6b1 feat: update SetTimeProjectCommandHandler to set status to Incomplete when additional time is added 2026-01-07 18:34:37 +03:30
7423391003 Merge branch 'Feature/program-manager/priority'
# Conflicts:
#	ServiceHost/Areas/Admin/Controllers/ProgramManager/ProjectController.cs
2026-01-07 18:05:46 +03:30
SamSys
23b65cfbfe GetSms Report Expand List 2026-01-07 16:59:21 +03:30
572f66f905 feat: implement auto-pending for task sections reaching estimated time 2026-01-07 16:52:50 +03:30
SamSys
48b75d2baa Sms Report get list init 2026-01-07 16:29:03 +03:30
140414b866 feat: add SetIsSent endpoint to update contract send status 2026-01-07 16:23:11 +03:30
4ade9e12a6 feat: add InstitutionContractIsSentFlag to track contract send status 2026-01-07 15:03:21 +03:30
SamSys
63edb33bf5 Sms Report Init 2026-01-07 14:49:44 +03:30
dd7e816767 feat: add SetContractSendFlag method and related request for contract send tracking 2026-01-07 14:46:34 +03:30
1deeff996f feat: implement InstitutionContractSendFlag repository and model for contract send tracking 2026-01-07 14:35:17 +03:30
2bea265989 feat: implement task priority change command and update project/task DTOs 2026-01-07 12:11:24 +03:30
ef9b78b924 Merge branch 'refs/heads/master' into Feature/program-manager/priority 2026-01-07 12:01:38 +03:30
8ad296fe61 Merge branch 'Feature/program-manager/fix-bugs' 2026-01-07 11:36:38 +03:30
SamSys
823110ea74 change 2026-01-07 11:24:27 +03:30
061058cbeb fix change status error 2026-01-07 11:21:58 +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
c6ed46d8b7 Merge remote-tracking branch 'origin/master' 2026-01-06 21:48:41 +03:30
3da7453ece feat: add assignment status tracking for projects, phases, and tasks 2026-01-06 21:47:22 +03:30
SamSys
9a591fabff change WorningSms methoth 2026-01-06 19:07:54 +03:30
9d09ef60f8 feat: add progress percentage calculations to project and task details 2026-01-06 18:19:16 +03:30
0757ac7e74 Merge branch 'refs/heads/master' into Feature/program-manager/set-Complete-on-Done 2026-01-06 14:22:28 +03:30
a9789023ac feat: add HTTP POST endpoint for ChangePriority method in ProjectController 2026-01-06 13:46:45 +03:30
34bd7ba444 add set file message to TaskChatMessage.cs 2026-01-06 12:21:12 +03:30
16b11a8bb8 feat: add ChangePriority method to ProjectController for task priority updates 2026-01-06 10:51:56 +03:30
SamSys
dd5455d80a ignore apsettings 2026-01-05 19:58:54 +03:30
9360dcad71 add UserSecretsId to ServiceHost.csproj 2026-01-05 19:14:11 +03:30
1971252713 feat: improve project board sorting by current user and task status 2026-01-05 17:52:12 +03:30
02cc099104 feat: update time calculation to include minutes in section time setting 2026-01-05 16:45:59 +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
582da511c6 feat: add progress percentage calculation to task sections 2026-01-01 19:25:28 +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
248 changed files with 11825 additions and 1987 deletions

6
.gitignore vendored
View File

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

View File

@@ -2,6 +2,8 @@
public enum TypeOfSmsSetting public enum TypeOfSmsSetting
{ {
//همه انواع پیامک
All = 0,
/// <summary> /// <summary>
/// پیامک /// پیامک
@@ -23,7 +25,7 @@ public enum TypeOfSmsSetting
/// <summary> /// <summary>
/// پیامک /// پیامک
/// هشدار اول /// هشدار بدهی
/// </summary> /// </summary>
Warning, Warning,
@@ -38,4 +40,14 @@ public enum TypeOfSmsSetting
/// </summary> /// </summary>
InstitutionContractConfirm, InstitutionContractConfirm,
/// <summary>
/// ارسال کد تاییدیه قرارداد مالی
/// </summary>
SendInstitutionContractConfirmationCode,
/// <summary>
/// یادآور وظایف
/// </summary>
TaskReminder,
} }

View File

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

View File

@@ -17,4 +17,18 @@ public class ApiResultViewModel
public string DeliveryUnixTime { get; set; } public string DeliveryUnixTime { get; set; }
public string DeliveryColor { get; set; } public string DeliveryColor { get; set; }
public string FullName { get; set; } public string FullName { get; set; }
}
public class ApiReportDto
{
public int MessageId { get; set; }
public long Mobile { get; set; }
public string SendUnixTime { get; set; }
public string DeliveryState { get; set; }
public string DeliveryUnixTime { get; set; }
public string DeliveryColor { get; set; }
} }

View File

@@ -19,6 +19,13 @@ public interface ISmsService
bool SendAccountsInfo(string number,string fullName, string userName); bool SendAccountsInfo(string number,string fullName, string userName);
Task<ApiResultViewModel> GetByMessageId(int messId); Task<ApiResultViewModel> GetByMessageId(int messId);
Task<List<ApiResultViewModel>> GetApiResult(string startDate, string endDate); Task<List<ApiResultViewModel>> GetApiResult(string startDate, string endDate);
#region ForApi
Task<List<ApiReportDto>> GetApiReport(string startDate, string endDate);
#endregion
string DeliveryStatus(byte? dv); string DeliveryStatus(byte? dv);
string DeliveryColorStatus(byte? dv); string DeliveryColorStatus(byte? dv);
string UnixTimeStampToDateTime(int? unixTimeStamp); string UnixTimeStampToDateTime(int? unixTimeStamp);

View File

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

View File

@@ -1,5 +1,6 @@
using _0_Framework.Application; using _0_Framework.Application;
using _0_Framework.Application.Enums;
using _0_Framework.Application.Sms; using _0_Framework.Application.Sms;
using Company.Domain.ContarctingPartyAgg; using Company.Domain.ContarctingPartyAgg;
using Company.Domain.InstitutionContractAgg; using Company.Domain.InstitutionContractAgg;
@@ -12,19 +13,21 @@ public class JobSchedulerRegistrator
private readonly IBackgroundJobClient _backgroundJobClient; private readonly IBackgroundJobClient _backgroundJobClient;
private readonly SmsReminder _smsReminder; private readonly SmsReminder _smsReminder;
private readonly IInstitutionContractRepository _institutionContractRepository; private readonly IInstitutionContractRepository _institutionContractRepository;
private readonly IInstitutionContractSmsServiceRepository _institutionContractSmsServiceRepository;
private static DateTime? _lastRunCreateTransaction; private static DateTime? _lastRunCreateTransaction;
private static DateTime? _lastRunSendMonthlySms; private static DateTime? _lastRunSendMonthlySms;
private readonly ISmsService _smsService; private readonly ISmsService _smsService;
private readonly ILogger<JobSchedulerRegistrator> _logger; private readonly ILogger<JobSchedulerRegistrator> _logger;
public JobSchedulerRegistrator(SmsReminder smsReminder, IBackgroundJobClient backgroundJobClient, IInstitutionContractRepository institutionContractRepository, ISmsService smsService, ILogger<JobSchedulerRegistrator> logger) public JobSchedulerRegistrator(SmsReminder smsReminder, IBackgroundJobClient backgroundJobClient, IInstitutionContractRepository institutionContractRepository, ISmsService smsService, ILogger<JobSchedulerRegistrator> logger, IInstitutionContractSmsServiceRepository institutionContractSmsServiceRepository)
{ {
_smsReminder = smsReminder; _smsReminder = smsReminder;
_backgroundJobClient = backgroundJobClient; _backgroundJobClient = backgroundJobClient;
_institutionContractRepository = institutionContractRepository; _institutionContractRepository = institutionContractRepository;
_smsService = smsService; _smsService = smsService;
_logger = logger; _logger = logger;
_institutionContractSmsServiceRepository = institutionContractSmsServiceRepository;
} }
public void Register() public void Register()
@@ -69,6 +72,32 @@ public class JobSchedulerRegistrator
() => SendLegalActionSms(), () => SendLegalActionSms(),
"*/1 * * * *" // هر 1 دقیقه یکبار چک کن "*/1 * * * *" // هر 1 دقیقه یکبار چک کن
); );
RecurringJob.AddOrUpdate(
"InstitutionContract.Block",
() => Block(),
"*/30 * * * *" // هر 30 دقیقه یکبار چک کن
);
RecurringJob.AddOrUpdate(
"InstitutionContract.UnBlock",
() => UnBlock(),
"*/10 * * * *"
);
RecurringJob.AddOrUpdate(
"InstitutionContract.DeActiveInstitutionEndOfContract",
() => DeActiveInstitutionEndOfContract(),
"*/30 * * * *"
);
RecurringJob.AddOrUpdate(
"InstitutionContract.BlueDeActiveAfterZeroDebt",
() => BlueDeActiveAfterZeroDebt(),
"*/10 * * * *"
);
} }
@@ -79,14 +108,14 @@ public class JobSchedulerRegistrator
[DisableConcurrentExecution(timeoutInSeconds: 1200)] [DisableConcurrentExecution(timeoutInSeconds: 1200)]
public async System.Threading.Tasks.Task CreateFinancialTransaction() public async System.Threading.Tasks.Task CreateFinancialTransaction()
{ {
var now =DateTime.Now; var now = DateTime.Now;
var endOfMonth = now.ToFarsi().FindeEndOfMonth(); var endOfMonth = now.ToFarsi().FindeEndOfMonth();
var endOfMonthGr = endOfMonth.ToGeorgianDateTime(); var endOfMonthGr = endOfMonth.ToGeorgianDateTime();
_logger.LogInformation("CreateFinancialTransaction job run"); _logger.LogInformation("CreateFinancialTransaction job run");
if (now.Date == endOfMonthGr.Date && now.Hour >= 2 && now.Hour < 4 && if (now.Date == endOfMonthGr.Date && now.Hour >= 2 && now.Hour < 4 &&
now.Date != _lastRunCreateTransaction?.Date) now.Date != _lastRunCreateTransaction?.Date)
{ {
var month = endOfMonth.Substring(5, 2); var month = endOfMonth.Substring(5, 2);
var year = endOfMonth.Substring(0, 4); var year = endOfMonth.Substring(0, 4);
var monthName = month.ToFarsiMonthByNumber(); var monthName = month.ToFarsiMonthByNumber();
@@ -101,17 +130,17 @@ public class JobSchedulerRegistrator
try try
{ {
await _institutionContractRepository.CreateTransactionForInstitutionContracts(endNewGr, endNewFa, description); await _institutionContractRepository.CreateTransactionForInstitutionContracts(endNewGr, endNewFa, description);
_lastRunCreateTransaction = now; _lastRunCreateTransaction = now;
Console.WriteLine("CreateTransAction executed"); Console.WriteLine("CreateTransAction executed");
} }
catch (Exception e) catch (Exception e)
{ {
await _smsService.Alarm("09114221321", "خطا-ایجاد سند مالی"); await _smsService.Alarm("09114221321", "خطا-ایجاد سند مالی");
} }
} }
} }
@@ -134,7 +163,7 @@ public class JobSchedulerRegistrator
try try
{ {
await _institutionContractRepository.SendMonthlySms(now); await _institutionContractSmsServiceRepository.SendMonthlySms(now);
_lastRunSendMonthlySms = now; _lastRunSendMonthlySms = now;
Console.WriteLine("Send Monthly sms executed"); Console.WriteLine("Send Monthly sms executed");
@@ -156,7 +185,7 @@ public class JobSchedulerRegistrator
public async System.Threading.Tasks.Task SendReminderSms() public async System.Threading.Tasks.Task SendReminderSms()
{ {
_logger.LogInformation("SendReminderSms job run"); _logger.LogInformation("SendReminderSms job run");
await _institutionContractRepository.SendReminderSmsForBackgroundTask(); await _institutionContractSmsServiceRepository.SendReminderSmsForBackgroundTask();
} }
/// <summary> /// <summary>
@@ -167,7 +196,7 @@ public class JobSchedulerRegistrator
public async System.Threading.Tasks.Task SendBlockSms() public async System.Threading.Tasks.Task SendBlockSms()
{ {
_logger.LogInformation("SendBlockSms job run"); _logger.LogInformation("SendBlockSms job run");
await _institutionContractRepository.SendBlockSmsForBackgroundTask(); await _institutionContractSmsServiceRepository.SendBlockSmsForBackgroundTask();
} }
@@ -179,7 +208,7 @@ public class JobSchedulerRegistrator
public async System.Threading.Tasks.Task SendInstitutionContractConfirmSms() public async System.Threading.Tasks.Task SendInstitutionContractConfirmSms()
{ {
_logger.LogInformation("SendInstitutionContractConfirmSms job run"); _logger.LogInformation("SendInstitutionContractConfirmSms job run");
await _institutionContractRepository.SendInstitutionContractConfirmSmsTask(); await _institutionContractSmsServiceRepository.SendInstitutionContractConfirmSmsTask();
} }
/// <summary> /// <summary>
@@ -190,14 +219,86 @@ public class JobSchedulerRegistrator
public async System.Threading.Tasks.Task SendWarningSms() public async System.Threading.Tasks.Task SendWarningSms()
{ {
_logger.LogInformation("SendWarningSms job run"); _logger.LogInformation("SendWarningSms job run");
await _institutionContractRepository.SendWarningSmsTask(); await _institutionContractSmsServiceRepository.SendWarningOrLegalActionSmsTask(TypeOfSmsSetting.Warning);
} }
/// <summary>
/// پیامک اقدام قضایی
/// </summary>
/// <returns></returns>
[DisableConcurrentExecution(timeoutInSeconds: 100)] [DisableConcurrentExecution(timeoutInSeconds: 100)]
public async System.Threading.Tasks.Task SendLegalActionSms() public async System.Threading.Tasks.Task SendLegalActionSms()
{ {
_logger.LogInformation("SendWarningSms job run"); _logger.LogInformation("SendWarningSms job run");
await _institutionContractRepository.SendLegalActionSmsTask(); await _institutionContractSmsServiceRepository.SendWarningOrLegalActionSmsTask(TypeOfSmsSetting.LegalAction);
}
/// <summary>
/// بلاگ سازی
/// </summary>
/// <returns></returns>
[DisableConcurrentExecution(timeoutInSeconds: 100)]
public async System.Threading.Tasks.Task Block()
{
_logger.LogInformation("block job run");
var now = DateTime.Now;
var executeDate = now.ToFarsi().Substring(8, 2);
if (executeDate == "20")
{
if (now.Hour >= 9 && now.Hour < 10)
{
await _institutionContractSmsServiceRepository.Block(now);
}
}
}
/// <summary>
/// آنبلاک
/// </summary>
/// <returns></returns>
[DisableConcurrentExecution(timeoutInSeconds: 100)]
public async System.Threading.Tasks.Task UnBlock()
{
_logger.LogInformation("UnBlock job run");
await _institutionContractSmsServiceRepository.UnBlock();
}
/// <summary>
/// غیر فعال سازی قراداد های پایان یافته
/// </summary>
/// <returns></returns>
[DisableConcurrentExecution(timeoutInSeconds: 100)]
public async System.Threading.Tasks.Task DeActiveInstitutionEndOfContract()
{
_logger.LogInformation("DeActiveInstitutionEndOfContract job run");
var now = DateTime.Now;
var executeDate = now.ToFarsi().Substring(8, 2);
if (executeDate == "01")
{
if (now.Hour >= 9 && now.Hour < 10)
{
await _institutionContractSmsServiceRepository.DeActiveInstitutionEndOfContract(now);
}
}
}
/// <summary>
/// غیرفعال سازس قرارداد های آبی که بدهی ندارند
/// </summary>
/// <returns></returns>
[DisableConcurrentExecution(timeoutInSeconds: 800)]
public async System.Threading.Tasks.Task BlueDeActiveAfterZeroDebt()
{
_logger.LogInformation("BlueDeActiveAfterZeroDebt job run");
await _institutionContractSmsServiceRepository.BlueDeActiveAfterZeroDebt();
} }
} }

View File

@@ -91,65 +91,7 @@ public interface IInstitutionContractRepository : IRepository<long, InstitutionC
Task<List<InstitutionContractPrintViewModel>> PrintAllAsync(List<long> ids); Task<List<InstitutionContractPrintViewModel>> PrintAllAsync(List<long> ids);
#region ReminderSMS
/// <summary>
/// دریافت لیست - ارسال پیامک
/// فراخوانی از سمت بک گراند سرویس
/// </summary>
/// <returns></returns>
Task<bool> SendReminderSmsForBackgroundTask();
/// <summary>
/// ارسال پیامک صورت حساب ماهانه
/// </summary>
/// <param name="now"></param>
/// <returns></returns>
Task SendMonthlySms(DateTime now);
/// <summary>
/// ارسال پیامک مسدودی از طرف بک گراند سرویس
/// </summary>
/// <returns></returns>
Task SendBlockSmsForBackgroundTask();
/// <summary>
/// دریافت لیست واجد شرایط بلاک
/// جهت ارسال پیامک مسدودی
/// </summary>
/// <param name="checkDate"></param>
/// <returns></returns>
Task<List<BlockSmsListData>> GetBlockListData(DateTime checkDate);
/// <summary>
/// ارسال پیامک مسدودی
/// </summary>
/// <param name="smsListData"></param>
/// <param name="typeOfSms"></param>
/// <param name="sendMessStart"></param>
/// <param name="sendMessEnd"></param>
/// <returns></returns>
Task SendBlockSmsToContractingParties(List<BlockSmsListData> smsListData, string typeOfSms,
string sendMessStart, string sendMessEnd);
/// <summary>
///دریافت لیست بدهکارن
/// جهت ارسال پیامک
/// </summary>
/// <returns></returns>
Task<List<SmsListData>> GetSmsListData(DateTime checkDate, TypeOfSmsSetting typeOfSmsSetting);
/// <summary>
/// ارسال پیامک های یاد آور بدهی
/// </summary>
/// <returns></returns>
Task SendReminderSmsToContractingParties(List<SmsListData> smsListData, string typeOfSms, string sendMessStart, string sendMessEnd);
/// <summary>
/// ارسال پیامک یادآور تایید قراداد مالی
/// </summary>
/// <returns></returns>
Task SendInstitutionContractConfirmSmsTask();
#endregion
#region CreateMontlyTransaction #region CreateMontlyTransaction
@@ -162,24 +104,12 @@ public interface IInstitutionContractRepository : IRepository<long, InstitutionC
#endregion #endregion
#region WarningSms
/// <summary>
/// پیامک های هشدار
/// </summary>
/// <returns></returns>
Task SendWarningSmsTask();
#endregion
#region legalAction
/// <summary>
/// پیامک اقدام قضائی
/// </summary>
/// <returns></returns>
Task SendLegalActionSmsTask();
#endregion
Task<long> GetIdByInstallmentId(long installmentId); Task<long> GetIdByInstallmentId(long installmentId);
Task<InstitutionContract> GetPreviousContract(long currentInstitutionContractId); Task<InstitutionContract> GetPreviousContract(long currentInstitutionContractId);

View File

@@ -0,0 +1,145 @@
using _0_Framework.Application.Enums;
using _0_Framework.Domain;
using CompanyManagment.App.Contracts.InstitutionContract;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Company.Domain.InstitutionContractAgg;
public interface IInstitutionContractSmsServiceRepository : IRepository<long, InstitutionContract>
{
#region reminderSMs
/// <summary>
/// ارسال پیامک یادآور تایید قراداد مالی
/// </summary>
/// <returns></returns>
Task SendInstitutionContractConfirmSmsTask();
#endregion
//هشدار و اقدام قضایی
#region WarningOrLegalActionSmsListData
/// <summary>
/// اجرای تسک پیامک هشدار یا اقدام قضایی
/// </summary>
/// <param name="typeOfSmsSetting"></param>
/// <returns></returns>
Task SendWarningOrLegalActionSmsTask(TypeOfSmsSetting typeOfSmsSetting);
/// <summary>
/// دریافت لیست بدهکاران آبی جهت هشدار یا اقدام قضایی
/// </summary>
/// <param name="typeOfSmsSetting"></param>
/// <returns></returns>
Task<List<SmsListData>> GetWarningOrLegalActionSmsListData(TypeOfSmsSetting typeOfSmsSetting);
/// <summary>
/// ارسال پیامک هشدار یا اقدام قضایی
/// </summary>
/// <param name="smsListData"></param>
/// <param name="typeOfSmsSetting"></param>
/// <returns></returns>
Task SendWarningOrLegalActionSms(List<SmsListData> smsListData, TypeOfSmsSetting typeOfSmsSetting);
#endregion
//بلاک - آنبلاک - پیامک بلاک -
// غیر فعال سازی قراداد های پایان یافته
#region Block
/// <summary>
/// ارسال پیامک مسدودی از طرف بک گراند سرویس
/// </summary>
/// <returns></returns>
Task SendBlockSmsForBackgroundTask();
/// <summary>
/// دریافت لیست واجد شرایط بلاک
/// جهت ارسال پیامک مسدودی
/// </summary>
/// <param name="checkDate"></param>
/// <returns></returns>
Task<List<BlockSmsListData>> GetBlockListData(DateTime checkDate);
/// <summary>
/// ارسال پیامک مسدودی
/// </summary>
/// <param name="smsListData"></param>
/// <param name="typeOfSms"></param>
/// <param name="sendMessStart"></param>
/// <param name="sendMessEnd"></param>
/// <returns></returns>
Task SendBlockSmsToContractingParties(List<BlockSmsListData> smsListData, string typeOfSms,
string sendMessStart, string sendMessEnd);
/// <summary>
/// بلاک سازی
/// </summary>
/// <param name="checkDate"></param>
/// <returns></returns>
Task Block(DateTime checkDate);
/// <summary>
/// دریافت لیست بدهکارانی که باید بلاک شوند
/// </summary>
/// <param name="checkDate"></param>
/// <returns></returns>
Task<List<long>> GetToBeBlockList(DateTime checkDate);
/// <summary>
/// آنبلاک
/// </summary>
/// <returns></returns>
Task UnBlock();
/// <summary>
/// غیر فعالسازی قرارداد های پایان یافته
/// </summary>
/// <param name="checkDate"></param>
/// <returns></returns>
Task DeActiveInstitutionEndOfContract(DateTime checkDate);
/// <summary>
/// غیرفعال سازس قرارداد های آبی که بدهی ندارند
/// </summary>
/// <returns></returns>
Task BlueDeActiveAfterZeroDebt();
#endregion
#region ReminderSMS
/// <summary>
/// دریافت لیست - ارسال پیامک
/// فراخوانی از سمت بک گراند سرویس
/// </summary>
/// <returns></returns>
Task<bool> SendReminderSmsForBackgroundTask();
/// <summary>
/// ارسال پیامک صورت حساب ماهانه
/// </summary>
/// <param name="now"></param>
/// <returns></returns>
Task SendMonthlySms(DateTime now);
/// <summary>
///دریافت لیست بدهکارن
/// جهت ارسال پیامک
/// </summary>
/// <returns></returns>
Task<List<SmsListData>> GetSmsListData(DateTime checkDate, TypeOfSmsSetting typeOfSmsSetting);
/// <summary>
/// ارسال پیامک های یاد آور بدهی
/// </summary>
/// <returns></returns>
Task SendReminderSmsToContractingParties(List<SmsListData> smsListData, string typeOfSms, string sendMessStart, string sendMessEnd);
#endregion
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Company.Domain.InstitutionContractSendFlagAgg;
/// <summary>
/// Interface برای Repository مربوط به فلگ ارسال قرارداد
/// </summary>
public interface IInstitutionContractSendFlagRepository
{
/// <summary>
/// ایجاد یک رکورد جدید برای فلگ ارسال قرارداد
/// </summary>
Task Create(InstitutionContractSendFlag flag);
/// <summary>
/// بازیابی فلگ بر اساس شناسه قرارداد
/// </summary>
Task<InstitutionContractSendFlag> GetByContractId(long contractId);
/// <summary>
/// به‌روزرسانی فلگ ارسال
/// </summary>
Task Update(InstitutionContractSendFlag flag);
/// <summary>
/// بررسی اینکه آیا قرارداد ارسال شده است
/// </summary>
Task<bool> IsContractSent(long contractId);
}

View File

@@ -0,0 +1,82 @@
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Company.Domain.InstitutionContractSendFlagAgg;
/// <summary>
/// نمایندگی فلگ ارسال قرارداد در MongoDB
/// این موجودیت برای ردیابی اینکه آیا قرارداد ارسال شده است استفاده می‌شود
/// </summary>
public class InstitutionContractSendFlag
{
public InstitutionContractSendFlag(long institutionContractId,bool isSent)
{
Id = Guid.NewGuid();
InstitutionContractId = institutionContractId;
IsSent = isSent;
CreatedDate = DateTime.Now;
}
/// <summary>
/// شناسه یکتای MongoDB
/// </summary>
[BsonId]
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; }
/// <summary>
/// شناسه قرارداد در SQL
/// </summary>
public long InstitutionContractId { get; set; }
/// <summary>
/// آیا قرارداد ارسال شده است
/// </summary>
public bool IsSent { get; set; }
/// <summary>
/// تاریخ و زمان ارسال
/// </summary>
public DateTime? SentDate { get; set; }
/// <summary>
/// تاریخ و زمان ایجاد رکورد
/// </summary>
public DateTime CreatedDate { get; set; }
/// <summary>
/// تاریخ و زمان آخرین به‌روزرسانی
/// </summary>
public DateTime? LastModifiedDate { get; set; }
/// <summary>
/// علامت‌گذاری قرارداد به عنوان ارسال‌شده
/// </summary>
public void MarkAsSent()
{
IsSent = true;
SentDate = DateTime.Now;
LastModifiedDate = DateTime.Now;
}
/// <summary>
/// بازگردانی علامت ارسال
/// </summary>
public void MarkAsNotSent()
{
IsSent = false;
SentDate = null;
LastModifiedDate = DateTime.Now;
}
/// <summary>
/// به‌روزرسانی علامت آخری اصلاح
/// </summary>
public void UpdateLastModified()
{
LastModifiedDate = DateTime.Now;
}
}

View File

@@ -15,12 +15,11 @@ public interface ISalaryAidRepository:IRepository<long,SalaryAid>
void RemoveRange(IEnumerable<SalaryAid> salaryAids); void RemoveRange(IEnumerable<SalaryAid> salaryAids);
#region Pooya #region Pooya
/// <summary> /// <summary>
/// گروهبندی بر اساس ماه هنگام جستجو با انتخاب کارمند /// گروهبندی بر اساس ماه هنگام جستجو با انتخاب کارمند
/// </summary> /// </summary>
SalaryAidsGroupedViewModel GetSearchListAsGrouped(SalaryAidSearchViewModel searchModel); SalaryAidsGroupedViewModel GetSearchListAsGrouped(SalaryAidSearchViewModel searchModel);
#endregion #endregion
} }

View File

@@ -1,10 +1,30 @@
using CompanyManagment.App.Contracts.SmsResult; using _0_Framework.Domain;
using CompanyManagment.App.Contracts.SmsResult;
using CompanyManagment.App.Contracts.SmsResult.Dto;
using System.Collections.Generic; using System.Collections.Generic;
using _0_Framework.Domain; using System.Threading.Tasks;
namespace Company.Domain.SmsResultAgg; namespace Company.Domain.SmsResultAgg;
public interface ISmsResultRepository : IRepository<long, SmsResult> public interface ISmsResultRepository : IRepository<long, SmsResult>
{ {
#region ForApi
/// <summary>
/// دریافت لیست پیامکها
/// </summary>
/// <param name="command"></param>
/// <returns></returns>
Task<List<SmsReportDto>> GetSmsReportList(SmsReportSearchModel searchModel);
/// <summary>
/// دریافت اکسپند لیست هر تاریخ
/// </summary>
/// <param name="searchModel"></param>
/// <param name="date"></param>
/// <returns></returns>
Task<List<SmsReportListDto>> GetSmsReportExpandList(SmsReportSearchModel searchModel, string date);
#endregion
List<SmsResultViewModel> Search(SmsResultSearchModel searchModel); List<SmsResultViewModel> Search(SmsResultSearchModel searchModel);
} }

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Company.Domain.InstitutionContractSendFlagAgg;
using MongoDB.Driver;
namespace CompanyManagement.Infrastructure.Mongo.InstitutionContractSendFlagRepo;
/// <summary>
/// Repository برای مدیریت فلگ ارسال قرارداد در MongoDB
/// </summary>
public class InstitutionContractSendFlagRepository : IInstitutionContractSendFlagRepository
{
private readonly IMongoCollection<InstitutionContractSendFlag> _collection;
public InstitutionContractSendFlagRepository(IMongoDatabase database)
{
_collection = database.GetCollection<InstitutionContractSendFlag>("InstitutionContractSendFlag");
}
public async Task Create(InstitutionContractSendFlag flag)
{
await _collection.InsertOneAsync(flag);
}
public async Task<InstitutionContractSendFlag> GetByContractId(long contractId)
{
var filter = Builders<InstitutionContractSendFlag>.Filter
.Eq(x => x.InstitutionContractId, contractId);
return await _collection.Find(filter).FirstOrDefaultAsync();
}
public async Task Update(InstitutionContractSendFlag flag)
{
var filter = Builders<InstitutionContractSendFlag>.Filter
.Eq(x => x.InstitutionContractId, flag.InstitutionContractId);
await _collection.ReplaceOneAsync(filter, flag);
}
public async Task<bool> IsContractSent(long contractId)
{
var flag = await GetByContractId(contractId);
return flag != null && flag.IsSent;
}
public async Task Remove(long contractId)
{
var filter = Builders<InstitutionContractSendFlag>.Filter
.Eq(x => x.InstitutionContractId, contractId);
await _collection.DeleteOneAsync(filter);
}
}

View File

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

View File

@@ -96,6 +96,8 @@ public class GetInstitutionContractListItemsViewModel
/// مبلغ قسط /// مبلغ قسط
/// </summary> /// </summary>
public double InstallmentAmount { get; set; } public double InstallmentAmount { get; set; }
public bool InstitutionContractIsSentFlag { get; set; }
} }
public class InstitutionContractListWorkshop public class InstitutionContractListWorkshop

View File

@@ -148,7 +148,7 @@ public interface IInstitutionContractApplication
/// <param name="id">شناسه قرارداد</param> /// <param name="id">شناسه قرارداد</param>
/// <returns>نتیجه عملیات</returns> /// <returns>نتیجه عملیات</returns>
OperationResult UnSign(long id); OperationResult UnSign(long id);
/// <summary> /// <summary>
/// ایجاد حساب کاربری برای طرف قرارداد /// ایجاد حساب کاربری برای طرف قرارداد
/// </summary> /// </summary>
@@ -305,6 +305,14 @@ public interface IInstitutionContractApplication
Task<InstitutionContractDiscountResponse> SetDiscountForCreation(InstitutionContractSetDiscountForCreationRequest request); Task<InstitutionContractDiscountResponse> SetDiscountForCreation(InstitutionContractSetDiscountForCreationRequest request);
Task<InstitutionContractDiscountResponse> ResetDiscountForCreation(InstitutionContractResetDiscountForExtensionRequest request); Task<InstitutionContractDiscountResponse> ResetDiscountForCreation(InstitutionContractResetDiscountForExtensionRequest request);
Task<OperationResult> CreationComplete(InstitutionContractExtensionCompleteRequest request); Task<OperationResult> CreationComplete(InstitutionContractExtensionCompleteRequest request);
/// <summary>
/// تعیین فلگ ارسال قرارداد در MongoDB
/// اگر فلگ وجود نداشتن‌د ایجاد می‌کند
/// </summary>
/// <param name="request">درخواست تعیین فلگ</param>
/// <returns>نتیجه عملیات</returns>
Task<OperationResult> SetContractSendFlag(SetInstitutionContractSendFlagRequest request);
} }
public class CreationSetContractingPartyResponse public class CreationSetContractingPartyResponse

View File

@@ -0,0 +1,19 @@
namespace CompanyManagment.App.Contracts.InstitutionContract;
/// <summary>
/// درخواست برای تعیین فلگ ارسال قرارداد
/// </summary>
public class SetInstitutionContractSendFlagRequest
{
/// <summary>
/// شناسه قرارداد
/// </summary>
public long InstitutionContractId { get; set; }
/// <summary>
/// آیا قرارداد ارسال شده است
/// </summary>
public bool IsSent { get; set; }
}

View File

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

View File

@@ -0,0 +1,15 @@
namespace CompanyManagment.App.Contracts.SmsResult.Dto;
/// <summary>
/// وضعیت ارسال پیامک
/// </summary>
public enum SendStatus
{
All=0,
/// <summary>
/// موفق
/// </summary>
Success,
//ناموفق
Failed,
}

View File

@@ -0,0 +1,54 @@
using System;
namespace CompanyManagment.App.Contracts.SmsResult.Dto;
public class SmsReportDto
{
/// <summary>
/// تاریخ ارسال
/// </summary>
public string SentDate { get; set; }
}
public class SmsReportListDto
{
/// <summary>
/// آی دی
/// </summary>
public long Id { get; set; }
/// <summary>
/// آی دی پیامک در sms.ir
/// </summary>
public int MessageId { get; set; }
/// <summary>
/// وضعیت ارسال
/// </summary>
public string Status { get; set; }
/// <summary>
/// نوع پیامک
/// </summary>
public string TypeOfSms { get; set; }
/// <summary>
/// نام طرف حساب
/// </summary>
public string ContractingPartyName { get; set; }
/// <summary>
/// شماره موبایل
/// </summary>
public string Mobile { get; set; }
/// <summary>
/// ساعت و دقیقه
/// </summary>
public string HourAndMinute { get; set; }
}

View File

@@ -0,0 +1,43 @@
using _0_Framework.Application.Enums;
namespace CompanyManagment.App.Contracts.SmsResult.Dto;
public class SmsReportSearchModel
{
//نوع پیامک
public TypeOfSmsSetting TypeOfSms { get; set; }
/// <summary>
/// وضعیت ارسال پیامک
/// </summary>
public SendStatus SendStatus { get; set; }
/// <summary>
/// شماره موبایل
/// </summary>
public string Mobile { get; set; }
/// <summary>
/// آی دی طرف حساب
/// </summary>
public long ContractingPatyId { get; set; }
/// <summary>
/// سال
/// </summary>
public string Year { get; set; }
/// <summary>
/// ماه
/// </summary>
public string Month { get; set; }
/// <summary>
/// تاریخ شروع
/// </summary>
public string StartDateFa { get; set; }
/// <summary>
/// تاریخ پایان
/// </summary>
public string EndDateFa { get; set; }
}

View File

@@ -1,14 +1,34 @@
using System; using _0_Framework.Application;
using CompanyManagment.App.Contracts.SmsResult.Dto;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using _0_Framework.Application;
namespace CompanyManagment.App.Contracts.SmsResult; namespace CompanyManagment.App.Contracts.SmsResult;
public interface ISmsResultApplication public interface ISmsResultApplication
{ {
#region ForApi
/// <summary>
/// دریافت لیست پیامکها
/// </summary>
/// <param name="searchModel"></param>
/// <returns></returns>
Task<List<SmsReportDto>> GetSmsReportList(SmsReportSearchModel searchModel);
/// <summary>
/// دریافت اکسپند لیست هر تاریخ
/// </summary>
/// <param name="searchModel"></param>
/// <param name="date"></param>
/// <returns></returns>
Task<List<SmsReportListDto>> GetSmsReportExpandList(SmsReportSearchModel searchModel, string date);
#endregion
OperationResult Create(CreateSmsResult command); OperationResult Create(CreateSmsResult command);
List<SmsResultViewModel> Search(SmsResultSearchModel searchModel); List<SmsResultViewModel> Search(SmsResultSearchModel searchModel);
} }

View File

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

View File

@@ -19,6 +19,7 @@ using Company.Domain.PaymentTransactionAgg;
using Company.Domain.RepresentativeAgg; using Company.Domain.RepresentativeAgg;
using Company.Domain.RollCallServiceAgg; using Company.Domain.RollCallServiceAgg;
using Company.Domain.WorkshopAgg; using Company.Domain.WorkshopAgg;
using Company.Domain.InstitutionContractSendFlagAgg;
using CompanyManagment.App.Contracts.FinancialInvoice; using CompanyManagment.App.Contracts.FinancialInvoice;
using CompanyManagment.App.Contracts.FinancialStatment; using CompanyManagment.App.Contracts.FinancialStatment;
using CompanyManagment.App.Contracts.InstitutionContract; using CompanyManagment.App.Contracts.InstitutionContract;
@@ -26,6 +27,7 @@ using CompanyManagment.App.Contracts.InstitutionContractContactinfo;
using CompanyManagment.App.Contracts.PaymentTransaction; using CompanyManagment.App.Contracts.PaymentTransaction;
using CompanyManagment.App.Contracts.SepehrPaymentGateway; using CompanyManagment.App.Contracts.SepehrPaymentGateway;
using CompanyManagment.App.Contracts.Workshop; using CompanyManagment.App.Contracts.Workshop;
using Microsoft.Extensions.Logging;
using PersianTools.Core; using PersianTools.Core;
using ConnectedPersonnelViewModel = CompanyManagment.App.Contracts.Workshop.ConnectedPersonnelViewModel; using ConnectedPersonnelViewModel = CompanyManagment.App.Contracts.Workshop.ConnectedPersonnelViewModel;
using FinancialStatment = Company.Domain.FinancialStatmentAgg.FinancialStatment; using FinancialStatment = Company.Domain.FinancialStatmentAgg.FinancialStatment;
@@ -50,6 +52,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
private readonly IPaymentTransactionRepository _paymentTransactionRepository; private readonly IPaymentTransactionRepository _paymentTransactionRepository;
private readonly IRollCallServiceRepository _rollCallServiceRepository; private readonly IRollCallServiceRepository _rollCallServiceRepository;
private readonly ISepehrPaymentGatewayService _sepehrPaymentGatewayService; private readonly ISepehrPaymentGatewayService _sepehrPaymentGatewayService;
private readonly IInstitutionContractSendFlagRepository _institutionContractSendFlagRepository;
public InstitutionContractApplication(IInstitutionContractRepository institutionContractRepository, public InstitutionContractApplication(IInstitutionContractRepository institutionContractRepository,
@@ -61,7 +64,8 @@ public class InstitutionContractApplication : IInstitutionContractApplication
IAccountApplication accountApplication, ISmsService smsService, IAccountApplication accountApplication, ISmsService smsService,
IFinancialInvoiceRepository financialInvoiceRepository, IHttpClientFactory httpClientFactory, IFinancialInvoiceRepository financialInvoiceRepository, IHttpClientFactory httpClientFactory,
IPaymentTransactionRepository paymentTransactionRepository, IRollCallServiceRepository rollCallServiceRepository, IPaymentTransactionRepository paymentTransactionRepository, IRollCallServiceRepository rollCallServiceRepository,
ISepehrPaymentGatewayService sepehrPaymentGatewayService) ISepehrPaymentGatewayService sepehrPaymentGatewayService,ILogger<SepehrPaymentGateway> sepehrGatewayLogger,
IInstitutionContractSendFlagRepository institutionContractSendFlagRepository)
{ {
_institutionContractRepository = institutionContractRepository; _institutionContractRepository = institutionContractRepository;
_contractingPartyRepository = contractingPartyRepository; _contractingPartyRepository = contractingPartyRepository;
@@ -78,7 +82,8 @@ public class InstitutionContractApplication : IInstitutionContractApplication
_paymentTransactionRepository = paymentTransactionRepository; _paymentTransactionRepository = paymentTransactionRepository;
_rollCallServiceRepository = rollCallServiceRepository; _rollCallServiceRepository = rollCallServiceRepository;
_sepehrPaymentGatewayService = sepehrPaymentGatewayService; _sepehrPaymentGatewayService = sepehrPaymentGatewayService;
_paymentGateway = new SepehrPaymentGateway(httpClientFactory); _paymentGateway = new SepehrPaymentGateway(httpClientFactory,sepehrGatewayLogger);
_institutionContractSendFlagRepository = institutionContractSendFlagRepository;
} }
public OperationResult Create(CreateInstitutionContract command) public OperationResult Create(CreateInstitutionContract command)
@@ -893,6 +898,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
return opration.Succcedded(); return opration.Succcedded();
} }
public void CreateContractingPartyAccount(long contractingPartyid, long accountId) public void CreateContractingPartyAccount(long contractingPartyid, long accountId)
{ {
_institutionContractRepository.CreateContractingPartyAccount(contractingPartyid, accountId); _institutionContractRepository.CreateContractingPartyAccount(contractingPartyid, accountId);
@@ -1284,9 +1290,6 @@ public class InstitutionContractApplication : IInstitutionContractApplication
financialInvoice = await _financialInvoiceRepository.GetUnPaidByEntityId(firstInstallment.Id, FinancialInvoiceItemType.BuyInstitutionContractInstallment); financialInvoice = await _financialInvoiceRepository.GetUnPaidByEntityId(firstInstallment.Id, FinancialInvoiceItemType.BuyInstitutionContractInstallment);
if (financialInvoice == null) if (financialInvoice == null)
{ {
var financialTransaction = new FinancialTransaction(0, today, today.ToFarsi(),
"قسط اول سرویس", "debt", "بابت خدمات", firstInstallmentAmount, 0, 0);
financialStatement.AddFinancialTransaction(financialTransaction);
invoiceAmount = firstInstallmentAmount; invoiceAmount = firstInstallmentAmount;
invoiceItemDescription = $"پرداخت قسط اول قرارداد شماره {institutionContract.ContractNo}"; invoiceItemDescription = $"پرداخت قسط اول قرارداد شماره {institutionContract.ContractNo}";
invoiceItemType = FinancialInvoiceItemType.BuyInstitutionContractInstallment; invoiceItemType = FinancialInvoiceItemType.BuyInstitutionContractInstallment;
@@ -1305,9 +1308,6 @@ public class InstitutionContractApplication : IInstitutionContractApplication
financialInvoice = await _financialInvoiceRepository.GetUnPaidByEntityId(institutionContract.id, FinancialInvoiceItemType.BuyInstitutionContract); financialInvoice = await _financialInvoiceRepository.GetUnPaidByEntityId(institutionContract.id, FinancialInvoiceItemType.BuyInstitutionContract);
if (financialInvoice == null) if (financialInvoice == null)
{ {
var financialTransaction = new FinancialTransaction(0, today, today.ToFarsi(),
"پرداخت کل سرویس", "debt", "بابت خدمات", institutionContract.TotalAmount, 0, 0);
financialStatement.AddFinancialTransaction(financialTransaction);
invoiceAmount = institutionContract.TotalAmount; invoiceAmount = institutionContract.TotalAmount;
invoiceItemDescription = $"پرداخت کل قرارداد شماره {institutionContract.ContractNo}"; invoiceItemDescription = $"پرداخت کل قرارداد شماره {institutionContract.ContractNo}";
invoiceItemType = FinancialInvoiceItemType.BuyInstitutionContract; invoiceItemType = FinancialInvoiceItemType.BuyInstitutionContract;
@@ -1825,7 +1825,60 @@ public class InstitutionContractApplication : IInstitutionContractApplication
installments.Add(lastInstallment); installments.Add(lastInstallment);
return installments; return installments;
} }
} }
/// <summary>
/// تعیین فلگ ارسال قرارداد
/// اگر فلگ وجود نداشتن‌د ایجاد می‌کند
/// </summary>
public async Task<OperationResult> SetContractSendFlag(SetInstitutionContractSendFlagRequest request)
{
var operationResult = new OperationResult();
try
{
// بازیابی قرارداد از SQL
var contract = _institutionContractRepository.Get(request.InstitutionContractId);
if (contract == null)
return operationResult.Failed("قرارداد مورد نظر یافت نشد");
// بررسی اینکه آیا فلگ در MongoDB وجود دارد
var existingFlag = await _institutionContractSendFlagRepository
.GetByContractId(request.InstitutionContractId);
if (existingFlag != null)
{
// اگر فلگ وجود داشتن‌د، آن را اپدیت کنیم
if (request.IsSent)
{
existingFlag.MarkAsSent();
}
else
{
existingFlag.MarkAsNotSent();
}
existingFlag.UpdateLastModified();
await _institutionContractSendFlagRepository.Update(existingFlag);
}
else
{
// اگر فلگ وجود ندارد، آن را ایجاد کنیم
var newFlag = new InstitutionContractSendFlag(
request.InstitutionContractId,
request.IsSent
);
await _institutionContractSendFlagRepository.Create(newFlag);
}
return operationResult.Succcedded();
}
catch (Exception ex)
{
return operationResult.Failed($"خطا در تعیین فلگ ارسال: {ex.Message}");
}
}
} }
#region CustomViewModels #region CustomViewModels

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using _0_Framework.Application; using _0_Framework.Application;
using Company.Domain.SmsResultAgg; using Company.Domain.SmsResultAgg;
using CompanyManagment.App.Contracts.SmsResult; using CompanyManagment.App.Contracts.SmsResult;
using CompanyManagment.App.Contracts.SmsResult.Dto;
namespace CompanyManagment.Application; namespace CompanyManagment.Application;
@@ -15,6 +17,23 @@ public class SmsResultApplication : ISmsResultApplication
_smsResultRepository = smsResultRepository; _smsResultRepository = smsResultRepository;
} }
#region ForApi
public async Task<List<SmsReportDto>> GetSmsReportList(SmsReportSearchModel searchModel)
{
return await _smsResultRepository.GetSmsReportList(searchModel);
}
public async Task<List<SmsReportListDto>> GetSmsReportExpandList(SmsReportSearchModel searchModel, string date)
{
return await _smsResultRepository.GetSmsReportExpandList(searchModel, date);
}
#endregion
public OperationResult Create(CreateSmsResult command) public OperationResult Create(CreateSmsResult command)
{ {
var op = new OperationResult(); var op = new OperationResult();

View File

@@ -1,13 +1,14 @@
using System; using _0_Framework.Application;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using _0_Framework.Application;
using _0_Framework.Application.Enums; using _0_Framework.Application.Enums;
using Company.Domain.InstitutionContractAgg; using Company.Domain.InstitutionContractAgg;
using Company.Domain.SmsResultAgg; using Company.Domain.SmsResultAgg;
using CompanyManagment.App.Contracts.InstitutionContract; using CompanyManagment.App.Contracts.InstitutionContract;
using CompanyManagment.App.Contracts.SmsResult; using CompanyManagment.App.Contracts.SmsResult;
using CompanyManagment.EFCore.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace CompanyManagment.Application; namespace CompanyManagment.Application;
@@ -15,11 +16,13 @@ public class SmsSettingApplication : ISmsSettingApplication
{ {
private readonly ISmsSettingsRepository _smsSettingsRepository; private readonly ISmsSettingsRepository _smsSettingsRepository;
private readonly IInstitutionContractRepository _institutionContractRepository; private readonly IInstitutionContractRepository _institutionContractRepository;
private readonly IInstitutionContractSmsServiceRepository _institutionContractSmsServiceRepository;
public SmsSettingApplication(ISmsSettingsRepository smsSettingsRepository, IInstitutionContractRepository institutionContractRepository) public SmsSettingApplication(ISmsSettingsRepository smsSettingsRepository, IInstitutionContractRepository institutionContractRepository, IInstitutionContractSmsServiceRepository institutionContractSmsServiceRepository)
{ {
_smsSettingsRepository = smsSettingsRepository; _smsSettingsRepository = smsSettingsRepository;
_institutionContractRepository = institutionContractRepository; _institutionContractRepository = institutionContractRepository;
_institutionContractSmsServiceRepository = institutionContractSmsServiceRepository;
} }
@@ -116,12 +119,12 @@ public class SmsSettingApplication : ISmsSettingApplication
public async Task<List<SmsListData>> GetSmsListData(TypeOfSmsSetting typeOfSmsSetting) public async Task<List<SmsListData>> GetSmsListData(TypeOfSmsSetting typeOfSmsSetting)
{ {
return await _institutionContractRepository.GetSmsListData(DateTime.Now, typeOfSmsSetting); return await _institutionContractSmsServiceRepository.GetSmsListData(DateTime.Now, typeOfSmsSetting);
} }
public async Task<List<BlockSmsListData>> GetBlockSmsListData(TypeOfSmsSetting typeOfSmsSetting) public async Task<List<BlockSmsListData>> GetBlockSmsListData(TypeOfSmsSetting typeOfSmsSetting)
{ {
return await _institutionContractRepository.GetBlockListData(DateTime.Now); return await _institutionContractSmsServiceRepository.GetBlockListData(DateTime.Now);
} }
@@ -134,7 +137,7 @@ public class SmsSettingApplication : ISmsSettingApplication
if (command.Any()) if (command.Any())
{ {
await _institutionContractRepository.SendReminderSmsToContractingParties(command, typeOfSms, sendMessStart, sendMessEnd); await _institutionContractSmsServiceRepository.SendReminderSmsToContractingParties(command, typeOfSms, sendMessStart, sendMessEnd);
return op.Succcedded(); return op.Succcedded();
} }
else else
@@ -153,7 +156,7 @@ public class SmsSettingApplication : ISmsSettingApplication
string sendMessEnd = "پایان مسدودی آنی "; string sendMessEnd = "پایان مسدودی آنی ";
if (command.Any()) if (command.Any())
{ {
await _institutionContractRepository.SendBlockSmsToContractingParties(command, typeOfSms, sendMessStart, await _institutionContractSmsServiceRepository.SendBlockSmsToContractingParties(command, typeOfSms, sendMessStart,
sendMessEnd); sendMessEnd);
return op.Succcedded(); return op.Succcedded();
} }

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,20 @@
using System.Collections.Generic; using _0_Framework.Application;
using System.Linq;
using _0_Framework.Application;
using _0_Framework.InfraStructure; using _0_Framework.InfraStructure;
using Company.Domain.SmsResultAgg; using Company.Domain.SmsResultAgg;
using CompanyManagment.App.Contracts.SmsResult; using CompanyManagment.App.Contracts.SmsResult;
using CompanyManagment.App.Contracts.SmsResult.Dto;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using _0_Framework.Application.Enums;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
namespace CompanyManagment.EFCore.Repository; namespace CompanyManagment.EFCore.Repository;
public class SmsResultRepository : RepositoryBase<long, SmsResult> , ISmsResultRepository public class SmsResultRepository : RepositoryBase<long, SmsResult>, ISmsResultRepository
{ {
private readonly CompanyContext _context; private readonly CompanyContext _context;
public SmsResultRepository(CompanyContext context) : base(context) public SmsResultRepository(CompanyContext context) : base(context)
@@ -15,9 +22,263 @@ public class SmsResultRepository : RepositoryBase<long, SmsResult> , ISmsResultR
_context = context; _context = context;
} }
#region ForApi
public async Task<List<SmsReportDto>> GetSmsReportList(SmsReportSearchModel searchModel)
{
// مرحله 1: همه رکوردها را با projection ساده بگیرید
var rawQuery = await _context.SmsResults
.Select(x => new
{
x.id,
x.ContractingPatyId,
x.Mobile,
x.Status,
x.TypeOfSms,
x.CreationDate,
DateOnly = x.CreationDate.Date // فقط تاریخ بدون ساعت
})
.AsNoTracking()
.ToListAsync(); // اینجا SQL اجرا می‌شود و همه داده‌ها به client می‌آیند
if (searchModel.ContractingPatyId > 0)
{
rawQuery = rawQuery.Where(x => x.ContractingPatyId == searchModel.ContractingPatyId).ToList();
}
if (!string.IsNullOrWhiteSpace(searchModel.Mobile))
{
rawQuery = rawQuery.Where(x => x.Mobile.Contains(searchModel.Mobile)).ToList();
}
if (searchModel.TypeOfSms != TypeOfSmsSetting.All && searchModel.TypeOfSms != TypeOfSmsSetting.Warning)
{
var typeOfSms = "All";
switch (searchModel.TypeOfSms)
{
case TypeOfSmsSetting.InstitutionContractDebtReminder:
typeOfSms = "یادآور بدهی ماهانه";
break;
case TypeOfSmsSetting.MonthlyInstitutionContract:
typeOfSms = "صورت حساب ماهانه";
break;
case TypeOfSmsSetting.BlockContractingParty:
typeOfSms = "اعلام مسدودی طرف حساب";
break;
case TypeOfSmsSetting.LegalAction:
typeOfSms = "اقدام قضایی";
break;
case TypeOfSmsSetting.InstitutionContractConfirm:
typeOfSms = "یادآور تایید قرارداد مالی";
break;
case TypeOfSmsSetting.SendInstitutionContractConfirmationCode:
typeOfSms = "کد تاییدیه قرارداد مالی";
break;
case TypeOfSmsSetting.TaskReminder:
typeOfSms = "یادآور وظایف";
break;
}
rawQuery = rawQuery.Where(x => x.TypeOfSms == typeOfSms).ToList();
}
if (searchModel.TypeOfSms == TypeOfSmsSetting.Warning)
{
rawQuery = rawQuery.Where(x => x.TypeOfSms.Contains("هشدار")).ToList();
}
if (searchModel.SendStatus != SendStatus.All)
{
var status = "All";
switch (searchModel.SendStatus)
{
case SendStatus.Success: status = "موفق";
break;
case SendStatus.Failed: status = "ناموفق";
break;
}
rawQuery = rawQuery.Where(x => x.Status == status).ToList();
}
#region searchByDate
if (!string.IsNullOrWhiteSpace(searchModel.StartDateFa) &&
!string.IsNullOrWhiteSpace(searchModel.EndDateFa))
{
if (searchModel.StartDateFa.TryToGeorgianDateTime(out var startGr) == false ||
searchModel.EndDateFa.TryToGeorgianDateTime(out var endGr) == false)
return new List<SmsReportDto>();
rawQuery = rawQuery.Where(x => x.CreationDate.Date >= startGr.Date && x.CreationDate.Date <= endGr.Date).ToList();
}
else
{
if (!string.IsNullOrWhiteSpace(searchModel.Year) && !string.IsNullOrWhiteSpace(searchModel.Month))
{
var start = searchModel.Year + "/" + searchModel.Month + "/01";
var end = start.FindeEndOfMonth();
var startGr = start.ToGeorgianDateTime();
var endGr = end.ToGeorgianDateTime();
rawQuery = rawQuery.Where(x => x.CreationDate.Date >= startGr.Date && x.CreationDate.Date <= endGr.Date).ToList();
}
else if (!string.IsNullOrWhiteSpace(searchModel.Year) && string.IsNullOrWhiteSpace(searchModel.Month))
{
var start = searchModel.Year + "/01/01";
var findEndOfYear = searchModel.Year + "/12/01";
var end = findEndOfYear.FindeEndOfMonth();
var startGr = start.ToGeorgianDateTime();
var endGr = end.ToGeorgianDateTime();
rawQuery = rawQuery.Where(x => x.CreationDate.Date >= startGr.Date && x.CreationDate.Date <= endGr.Date).ToList();
}
}
#endregion
// مرحله 2: گروه‌بندی و انتخاب آخرین رکورد هر روز روی Client
var grouped = rawQuery
.GroupBy(x => x.DateOnly)
.Select(g => g.OrderByDescending(x => x.CreationDate).First())
.OrderByDescending(x => x.CreationDate)
.ToList();
// مرحله 3: تبدیل به DTO و ToFarsi
var result = grouped.Select(x => new SmsReportDto
{
SentDate = x.CreationDate.ToFarsi()
}).ToList();
return result;
}
public async Task<List<SmsReportListDto>> GetSmsReportExpandList(SmsReportSearchModel searchModel, string date)
{
if(string.IsNullOrWhiteSpace(date))
return new List<SmsReportListDto>();
if (date.TryToGeorgianDateTime(out var searchDate) == false)
return new List<SmsReportListDto>();
var query = await _context.SmsResults.Where(x => x.CreationDate.Date == searchDate.Date)
.Select(x =>
new
{
x.id,
x.MessageId,
x.Status,
x.TypeOfSms,
x.ContractingPartyName,
x.Mobile,
x.ContractingPatyId,
x.InstitutionContractId,
x.CreationDate,
x.CreationDate.Hour,
x.CreationDate.Minute
}).AsNoTracking()
.ToListAsync(); ;
if (searchModel.ContractingPatyId > 0)
{
query = query.Where(x => x.ContractingPatyId == searchModel.ContractingPatyId).ToList();
}
if (!string.IsNullOrWhiteSpace(searchModel.Mobile))
{
query = query.Where(x => x.Mobile.Contains(searchModel.Mobile)).ToList();
}
if (searchModel.TypeOfSms != TypeOfSmsSetting.All && searchModel.TypeOfSms != TypeOfSmsSetting.Warning)
{
var typeOfSms = "All";
switch (searchModel.TypeOfSms)
{
case TypeOfSmsSetting.InstitutionContractDebtReminder:
typeOfSms = "یادآور بدهی ماهانه";
break;
case TypeOfSmsSetting.MonthlyInstitutionContract:
typeOfSms = "صورت حساب ماهانه";
break;
case TypeOfSmsSetting.BlockContractingParty:
typeOfSms = "اعلام مسدودی طرف حساب";
break;
case TypeOfSmsSetting.LegalAction:
typeOfSms = "اقدام قضایی";
break;
case TypeOfSmsSetting.InstitutionContractConfirm:
typeOfSms = "یادآور تایید قرارداد مالی";
break;
case TypeOfSmsSetting.SendInstitutionContractConfirmationCode:
typeOfSms = "کد تاییدیه قرارداد مالی";
break;
case TypeOfSmsSetting.TaskReminder:
typeOfSms = "یادآور وظایف";
break;
}
query = query.Where(x => x.TypeOfSms == typeOfSms).ToList();
}
if (searchModel.TypeOfSms == TypeOfSmsSetting.Warning)
{
query = query.Where(x => x.TypeOfSms.Contains("هشدار")).ToList();
}
if (searchModel.SendStatus != SendStatus.All)
{
var status = "All";
switch (searchModel.SendStatus)
{
case SendStatus.Success:
status = "موفق";
break;
case SendStatus.Failed:
status = "ناموفق";
break;
}
query = query.Where(x => x.Status == status).ToList();
}
if (query.Count == 0)
return new List<SmsReportListDto>();
var result = query.OrderByDescending(x => x.CreationDate.Hour)
.ThenByDescending(x => x.CreationDate.Minute).Select(x =>
new SmsReportListDto()
{
Id = x.id,
MessageId = x.MessageId,
Status = x.Status,
TypeOfSms = x.TypeOfSms,
ContractingPartyName = x.ContractingPartyName,
Mobile = x.Mobile,
HourAndMinute = x.CreationDate.TimeOfDay.ToString(@"hh\:mm"),
}).ToList();
return result;
}
#endregion
public List<App.Contracts.SmsResult.SmsResultViewModel> Search(SmsResultSearchModel searchModel) public List<App.Contracts.SmsResult.SmsResultViewModel> Search(SmsResultSearchModel searchModel)
{ {
var query = _context.SmsResults.Select(x => new App.Contracts.SmsResult.SmsResultViewModel() var query = _context.SmsResults.Select(x => new App.Contracts.SmsResult.SmsResultViewModel()
{ {
Id = x.id, Id = x.id,
@@ -64,7 +325,7 @@ public class SmsResultRepository : RepositoryBase<long, SmsResult> , ISmsResultR
var endGr = end.ToGeorgianDateTime(); var endGr = end.ToGeorgianDateTime();
query = query.Where(x => x.CreationDate.Date >= startGr.Date && x.CreationDate.Date <= endGr.Date); query = query.Where(x => x.CreationDate.Date >= startGr.Date && x.CreationDate.Date <= endGr.Date);
} }
else if (!string.IsNullOrWhiteSpace(searchModel.Year) && string.IsNullOrWhiteSpace(searchModel.Month)) else if (!string.IsNullOrWhiteSpace(searchModel.Year) && string.IsNullOrWhiteSpace(searchModel.Month))
{ {
@@ -74,7 +335,7 @@ public class SmsResultRepository : RepositoryBase<long, SmsResult> , ISmsResultR
var startGr = start.ToGeorgianDateTime(); var startGr = start.ToGeorgianDateTime();
var endGr = end.ToGeorgianDateTime(); var endGr = end.ToGeorgianDateTime();
query = query.Where(x => x.CreationDate.Date >= startGr.Date && x.CreationDate.Date <= endGr.Date); query = query.Where(x => x.CreationDate.Date >= startGr.Date && x.CreationDate.Date <= endGr.Date);
} }
} }
@@ -82,12 +343,12 @@ public class SmsResultRepository : RepositoryBase<long, SmsResult> , ISmsResultR
query = query.OrderByDescending(x => x.CreationDate) query = query.OrderByDescending(x => x.CreationDate)
.ThenByDescending(x=>x.CreationDate.Hour).ThenByDescending(x=>x.CreationDate.Minute); .ThenByDescending(x => x.CreationDate.Hour).ThenByDescending(x => x.CreationDate.Minute);
return query.Skip(searchModel.PageIndex).Take(30).ToList(); return query.Skip(searchModel.PageIndex).Take(30).ToList();
} }
} }

View File

@@ -207,16 +207,11 @@ public class SmsService : ISmsService
} }
public async Task<List<ApiResultViewModel>> GetApiResult(string startDate, string endDate) public async Task<List<ApiResultViewModel>> GetApiResult(string startDate, string endDate)
{ {
var st = new DateTime(2024, 6, 2);
var ed = new DateTime(2024, 7, 1);
if (!string.IsNullOrWhiteSpace(startDate) && startDate.Length == 10) if(startDate.TryToGeorgianDateTime(out var st) == false || endDate.TryToGeorgianDateTime(out var ed) == false)
{ return new List<ApiResultViewModel>();
st = startDate.ToGeorgianDateTime();
}
if (!string.IsNullOrWhiteSpace(endDate) && endDate.Length == 10)
{
ed = endDate.ToGeorgianDateTime();
}
var res = new List<ApiResultViewModel>(); var res = new List<ApiResultViewModel>();
Int32 unixTimestamp = (int)st.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; Int32 unixTimestamp = (int)st.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
Int32 unixTimestamp2 = (int)ed.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; Int32 unixTimestamp2 = (int)ed.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
@@ -248,6 +243,44 @@ public class SmsService : ISmsService
return res; return res;
} }
public async Task<List<ApiReportDto>> GetApiReport(string startDate, string endDate)
{
if (startDate.TryToGeorgianDateTime(out var st) == false || endDate.TryToGeorgianDateTime(out var ed) == false)
return new List<ApiReportDto>();
var res = new List<ApiReportDto>();
Int32 unixTimestamp = (int)st.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
Int32 unixTimestamp2 = (int)ed.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
// int? fromDateUnixTime = null; // unix time - for instance: 1700598600
//int? toDateUnixTime = null; // unix time - for instance: 1703190600
int pageNumber = 2;
int pageSize = 100; // max: 100
SmsIr smsIr = new SmsIr("Og5M562igmzJRhQPnq0GdtieYdLgtfikjzxOmeQBPxJjZtyge5Klc046Lfw1mxSa");
var response = await smsIr.GetArchivedReportAsync(pageNumber, pageSize, unixTimestamp, unixTimestamp2);
MessageReportResult[] messages = response.Data;
foreach (var message in messages)
{
var appendData = new ApiReportDto()
{
MessageId = message.MessageId,
Mobile = message.Mobile,
SendUnixTime = UnixTimeStampToDateTime(message.SendDateTime),
DeliveryState = DeliveryStatus(message.DeliveryState),
DeliveryUnixTime = UnixTimeStampToDateTime(message.DeliveryDateTime),
DeliveryColor = DeliveryColorStatus(message.DeliveryState),
};
res.Add(appendData);
}
return res;
}
public string DeliveryStatus(byte? dv) public string DeliveryStatus(byte? dv)
{ {
string mess = ""; string mess = "";

View File

@@ -10,6 +10,7 @@ using CompanyManagment.App.Contracts.AuthorizedPerson;
using _0_Framework.Application; using _0_Framework.Application;
using _0_Framework.Application.UID; using _0_Framework.Application.UID;
using Company.Application.Contracts.AuthorizedBankDetails; using Company.Application.Contracts.AuthorizedBankDetails;
using CompanyManagment.App.Contracts.AuthorizedBankDetails;
namespace CompanyManagment.EFCore.Services; 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}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackgroundInstitutionContract.Task", "BackgroundInstitutionContract\BackgroundInstitutionContract.Task\BackgroundInstitutionContract.Task.csproj", "{F78FBB92-294B-88BA-168D-F0C578B0D7D6}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProgramManager", "ProgramManager", "{67AFF7B6-4C4F-464C-A90D-9BDB644D83A9}" 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 EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{48F6F6A5-7340-42F8-9216-BEB7A4B7D5A1}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{48F6F6A5-7340-42F8-9216-BEB7A4B7D5A1}"
EndProject EndProject

View File

@@ -61,6 +61,7 @@ using Company.Domain.HolidayItemAgg;
using Company.Domain.InstitutionContractAgg; using Company.Domain.InstitutionContractAgg;
using Company.Domain.InstitutionContractContactInfoAgg; using Company.Domain.InstitutionContractContactInfoAgg;
using Company.Domain.InstitutionContractExtensionTempAgg; using Company.Domain.InstitutionContractExtensionTempAgg;
using Company.Domain.InstitutionContractSendFlagAgg;
using Company.Domain.InstitutionPlanAgg; using Company.Domain.InstitutionPlanAgg;
using Company.Domain.InsuranceAgg; using Company.Domain.InsuranceAgg;
using Company.Domain.InsuranceEmployeeInfoAgg; using Company.Domain.InsuranceEmployeeInfoAgg;
@@ -123,6 +124,7 @@ using Company.Domain.ZoneAgg;
using CompanyManagement.Infrastructure.Excel.SalaryAid; using CompanyManagement.Infrastructure.Excel.SalaryAid;
using CompanyManagement.Infrastructure.Mongo.EmployeeFaceEmbeddingRepo; using CompanyManagement.Infrastructure.Mongo.EmployeeFaceEmbeddingRepo;
using CompanyManagement.Infrastructure.Mongo.InstitutionContractInsertTempRepo; using CompanyManagement.Infrastructure.Mongo.InstitutionContractInsertTempRepo;
using CompanyManagement.Infrastructure.Mongo.InstitutionContractSendFlagRepo;
using CompanyManagment.App.Contracts.AdminMonthlyOverview; using CompanyManagment.App.Contracts.AdminMonthlyOverview;
using CompanyManagment.App.Contracts.AndroidApkVersion; using CompanyManagment.App.Contracts.AndroidApkVersion;
using CompanyManagment.App.Contracts.AuthorizedPerson; using CompanyManagment.App.Contracts.AuthorizedPerson;
@@ -236,6 +238,7 @@ using P_TextManager.Domin.TextManagerAgg;
using CompanyManagment.App.Contracts.PaymentCallback; using CompanyManagment.App.Contracts.PaymentCallback;
using CompanyManagment.App.Contracts.SepehrPaymentGateway; using CompanyManagment.App.Contracts.SepehrPaymentGateway;
using Company.Domain.CameraBugReportAgg; using Company.Domain.CameraBugReportAgg;
using CompanyManagment.App.Contracts.AuthorizedBankDetails;
using CompanyManagment.App.Contracts.CameraBugReport; using CompanyManagment.App.Contracts.CameraBugReport;
using CameraBugReportRepository = CompanyManagement.Infrastructure.Mongo.CameraBugReportRepo.CameraBugReportRepository; using CameraBugReportRepository = CompanyManagement.Infrastructure.Mongo.CameraBugReportRepo.CameraBugReportRepository;
using CompanyManagment.EFCore.Services; using CompanyManagment.EFCore.Services;
@@ -560,6 +563,7 @@ public class PersonalBootstrapper
services.AddTransient<ISmsSettingsRepository, SmsSettingsRepository>(); services.AddTransient<ISmsSettingsRepository, SmsSettingsRepository>();
services.AddTransient<ISmsSettingApplication, SmsSettingApplication>(); services.AddTransient<ISmsSettingApplication, SmsSettingApplication>();
services.AddTransient<IInstitutionContractSmsServiceRepository, InstitutionContractSmsServiceRepository>();
#endregion #endregion
@@ -657,6 +661,9 @@ public class PersonalBootstrapper
services.AddTransient<ICameraBugReportApplication, CameraBugReportApplication>(); services.AddTransient<ICameraBugReportApplication, CameraBugReportApplication>();
services.AddTransient<ICameraBugReportRepository, CameraBugReportRepository>(); // MongoDB Implementation services.AddTransient<ICameraBugReportRepository, CameraBugReportRepository>(); // MongoDB Implementation
// InstitutionContractSendFlag - MongoDB
services.AddTransient<IInstitutionContractSendFlagRepository, InstitutionContractSendFlagRepository>();
services.AddDbContext<CompanyContext>(x => x.UseSqlServer(connectionString)); services.AddDbContext<CompanyContext>(x => x.UseSqlServer(connectionString));
} }
} }

View File

@@ -9,6 +9,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation" Version="12.1.1" /> <PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="MediatR" Version="14.0.0" /> <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.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
</ItemGroup> </ItemGroup>
@@ -18,4 +19,10 @@
<ProjectReference Include="..\..\Domain\GozareshgirProgramManager.Domain\GozareshgirProgramManager.Domain.csproj" /> <ProjectReference Include="..\..\Domain\GozareshgirProgramManager.Domain\GozareshgirProgramManager.Domain.csproj" />
</ItemGroup> </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> </Project>

View File

@@ -209,22 +209,38 @@ public class CreateOrEditCheckoutCommandHandler : IBaseCommandHandler<CreateOrEd
} }
} }
//حقوق نهایی ////حقوق نهایی
var monthlySalaryPay = (totalHoursWorked * monthlySalaryDefined) / mandatoryHours; //var monthlySalaryPay = (totalHoursWorked * monthlySalaryDefined) / mandatoryHours;
// اگر اضافه کار داشت حقوق تعین شده به عنوان حقوق نهایی در نظر گرفته میشود //// اگر اضافه کار داشت حقوق تعین شده به عنوان حقوق نهایی در نظر گرفته میشود
monthlySalaryPay = monthlySalaryPay > monthlySalaryDefined ? monthlySalaryDefined : monthlySalaryPay; //monthlySalaryPay = monthlySalaryPay > monthlySalaryDefined ? monthlySalaryDefined : monthlySalaryPay;
//حقوق کسر شده ////حقوق کسر شده
var deductionFromSalary = monthlySalaryDefined - monthlySalaryPay; //var deductionFromSalary = monthlySalaryDefined - monthlySalaryPay;
//new chang salary compute
var monthlySalaryPay = totalHoursWorked * monthlySalaryDefined;
//زمان باقی مانده //زمان باقی مانده
var remainingTime = totalHoursWorked - mandatoryHours; var remainingTime = totalHoursWorked - mandatoryHours;
//تناسب به دقیقه
#region MyRegion
//var monthlySalaryDefinedTest = monthlySalaryDefined * mandatoryHours;
//var monthlySalaryPayTest = totalHoursWorked * monthlySalaryDefined;
////// اگر اضافه کار داشت حقوق تعین شده به عنوان حقوق نهایی در نظر گرفته میشود
//monthlySalaryPayTest = monthlySalaryPayTest > monthlySalaryDefinedTest ? monthlySalaryDefinedTest : monthlySalaryPayTest;
//////حقوق کسر شده
//var deductionFromSalaryTest = monthlySalaryDefinedTest - monthlySalaryPayTest;
#endregion
var computeResult = new ComputeResultDto var computeResult = new ComputeResultDto
{ {
MandatoryHours = mandatoryHours, MandatoryHours = mandatoryHours,
MonthlySalaryPay = monthlySalaryPay, MonthlySalaryPay = monthlySalaryPay,
DeductionFromSalary = deductionFromSalary, DeductionFromSalary = 0 /*deductionFromSalary*/,
RemainingHours = remainingTime RemainingHours = remainingTime
}; };
Console.WriteLine(mandatoryHours); Console.WriteLine(mandatoryHours);

View File

@@ -1,10 +1,11 @@
using GozareshgirProgramManager.Application._Common.Interfaces; using DNTPersianUtils.Core;
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models; using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.SalaryPaymentSettings.Queries.GetUserListWhoHaveSettings; using GozareshgirProgramManager.Application.Modules.SalaryPaymentSettings.Queries.GetUserListWhoHaveSettings;
using GozareshgirProgramManager.Domain._Common; using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.CheckoutAgg.Enums; using GozareshgirProgramManager.Domain.CheckoutAgg.Enums;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PersianTools.Core; using PersianDateTime = PersianTools.Core.PersianDateTime;
namespace GozareshgirProgramManager.Application.Modules.Checkouts.Queries.GetUserToGropCreate; namespace GozareshgirProgramManager.Application.Modules.Checkouts.Queries.GetUserToGropCreate;
@@ -45,8 +46,8 @@ public class GetUserToGroupCreatingQueryHandler : IBaseQueryHandler<GetUserToGro
"ایجاد فیش فقط برای ماه های گذشته امکان پذیر است"); "ایجاد فیش فقط برای ماه های گذشته امکان پذیر است");
var lastMonthStart = lastMonth; //var lastMonthStart = lastMonth;
var lastMonthEnd = lastMonth; var lastMonthEnd = ((selectedDate.ToFarsi().FindeEndOfMonth())).ToGeorgianDateTime();
var query = var query =
await (from u in _context.Users await (from u in _context.Users
@@ -60,8 +61,8 @@ public class GetUserToGroupCreatingQueryHandler : IBaseQueryHandler<GetUserToGro
// LEFT JOIN // LEFT JOIN
//فیش //فیش
join ch in _context.Checkouts join ch in _context.Checkouts
.Where(x => x.CheckoutStartDate < lastMonthStart .Where(x => x.CheckoutStartDate < lastMonthEnd
&& x.CheckoutEndDate >= lastMonthStart) && x.CheckoutEndDate > selectedDate)
on u.Id equals ch.UserId into chJoin on u.Id equals ch.UserId into chJoin
from ch in chJoin.DefaultIfEmpty() from ch in chJoin.DefaultIfEmpty()

View File

@@ -10,7 +10,7 @@ public record AddTaskToPhaseCommand(
Guid PhaseId, Guid PhaseId,
string Name, string Name,
string? Description = null, string? Description = null,
TaskPriority Priority = TaskPriority.Medium, ProjectTaskPriority Priority = ProjectTaskPriority.Medium,
int OrderIndex = 0, int OrderIndex = 0,
DateTime? DueDate = null DateTime? DueDate = null
) : IBaseCommand; ) : IBaseCommand;

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

@@ -2,6 +2,9 @@ using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models; using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common; using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Phase;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories; using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
using GozareshgirProgramManager.Domain.SkillAgg.Repositories; using GozareshgirProgramManager.Domain.SkillAgg.Repositories;

View File

@@ -0,0 +1,62 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.AutoPendingFullTimeTaskSections;
public record AutoPendingFullTimeTaskSectionsCommand : IBaseCommand;
public class AutoPendingFullTimeTaskSectionsCommandHandler : IBaseCommandHandler<AutoPendingFullTimeTaskSectionsCommand>
{
private readonly ITaskSectionRepository _taskSectionRepository;
private readonly IUnitOfWork _unitOfWork;
public AutoPendingFullTimeTaskSectionsCommandHandler(
ITaskSectionRepository taskSectionRepository,
IUnitOfWork unitOfWork)
{
_taskSectionRepository = taskSectionRepository;
_unitOfWork = unitOfWork;
}
public async Task<OperationResult> Handle(AutoPendingFullTimeTaskSectionsCommand request, CancellationToken cancellationToken)
{
try
{
// تمام سکشن‌هایی که هنوز Pending یا Completed نشده‌اند را دریافت کن
var taskSections = await _taskSectionRepository.GetAllNotCompletedOrPendingIncludeAllAsync(cancellationToken);
foreach (var section in taskSections)
{
var totalSpent = section.GetTotalTimeSpent();
var estimate = section.FinalEstimatedHours;
if (estimate.TotalMinutes <= 0)
continue; // تسک بدون تخمین را نادیده بگیر
if (totalSpent >= estimate)
{
// مهم: وضعیت را مستقل از فعال/غیرفعال بودن فعالیت‌ها PendingForCompletion کنیم
if (section.IsInProgress())
{
// اگر فعالیت فعال دارد، با وضعیت جدید متوقف شود
section.StopWork(TaskSectionStatus.PendingForCompletion, "اتمام خودکار - رسیدن به ۱۰۰٪ زمان تخمینی");
}
else
{
section.UpdateStatus(TaskSectionStatus.PendingForCompletion);
}
}
}
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
catch (Exception ex)
{
return OperationResult.Failure($"خطا در در انتظار تکمیل قرار دادن خودکار تسک‌ها: {ex.Message}");
}
}
}

View File

@@ -2,6 +2,7 @@ using System.Linq;
using GozareshgirProgramManager.Application._Common.Interfaces; using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models; using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Phase;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

View File

@@ -2,6 +2,7 @@ using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models; using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common; using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Phase;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories; using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeDeployStatusProject; namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeDeployStatusProject;

View File

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

View File

@@ -0,0 +1,111 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Phase;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeTaskPriority;
public record ChangeTaskPriorityCommand(
Guid Id,
ProjectHierarchyLevel Level,
ProjectTaskPriority Priority
) : IBaseCommand;
public class ChangeTaskPriorityCommandHandler : IBaseCommandHandler<ChangeTaskPriorityCommand>
{
private readonly IProjectTaskRepository _taskRepository;
private readonly IProjectPhaseRepository _phaseRepository;
private readonly IProjectRepository _projectRepository;
private readonly IUnitOfWork _unitOfWork;
public ChangeTaskPriorityCommandHandler(
IProjectTaskRepository taskRepository,
IProjectPhaseRepository phaseRepository,
IProjectRepository projectRepository,
IUnitOfWork unitOfWork)
{
_taskRepository = taskRepository;
_phaseRepository = phaseRepository;
_projectRepository = projectRepository;
_unitOfWork = unitOfWork;
}
public async Task<OperationResult> Handle(ChangeTaskPriorityCommand request, CancellationToken cancellationToken)
{
switch (request.Level)
{
case ProjectHierarchyLevel.Task:
return await HandleTaskLevelAsync(request.Id, request.Priority, cancellationToken);
case ProjectHierarchyLevel.Phase:
return await HandlePhaseLevelAsync(request.Id, request.Priority, cancellationToken);
case ProjectHierarchyLevel.Project:
return await HandleProjectLevelAsync(request.Id, request.Priority, cancellationToken);
default:
return OperationResult.Failure("سطح نامعتبر است");
}
}
// Task-level priority update
private async Task<OperationResult> HandleTaskLevelAsync(Guid taskId, ProjectTaskPriority priority, CancellationToken ct)
{
var task = await _taskRepository.GetByIdAsync(taskId, ct);
if (task is null)
return OperationResult.NotFound("تسک یافت نشد");
if (task.Priority != priority)
{
task.SetPriority(priority);
}
await _unitOfWork.SaveChangesAsync(ct);
return OperationResult.Success();
}
// Phase-level bulk priority update
private async Task<OperationResult> HandlePhaseLevelAsync(Guid phaseId, ProjectTaskPriority priority, CancellationToken ct)
{
var phase = await _phaseRepository.GetWithTasksAsync(phaseId);
if (phase is null)
return OperationResult.NotFound("فاز یافت نشد");
var tasks = phase.Tasks?.ToList() ?? new List<ProjectTask>();
foreach (var t in tasks)
{
if (t.Priority != priority)
{
t.SetPriority(priority);
}
}
await _unitOfWork.SaveChangesAsync(ct);
return OperationResult.Success();
}
// Project-level bulk priority update across all phases
private async Task<OperationResult> HandleProjectLevelAsync(Guid projectId, ProjectTaskPriority priority, CancellationToken ct)
{
var project = await _projectRepository.GetWithFullHierarchyAsync(projectId);
if (project is null)
return OperationResult.NotFound("پروژه یافت نشد");
var phases = project.Phases?.ToList() ?? new List<ProjectPhase>();
foreach (var phase in phases)
{
var tasks = phase.Tasks?.ToList() ?? new List<ProjectTask>();
foreach (var t in tasks)
{
if (t.Priority != priority)
{
t.SetPriority(priority);
}
}
}
await _unitOfWork.SaveChangesAsync(ct);
return OperationResult.Success();
}
}

View File

@@ -3,6 +3,9 @@ using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common; using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain._Common.Exceptions; using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Phase;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories; using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;

View File

@@ -2,6 +2,7 @@ using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models; using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common; using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories; using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
using MediatR; using MediatR;

View File

@@ -1,13 +1,21 @@
using GozareshgirProgramManager.Application._Common.Interfaces; using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application.Modules.Projects.DTOs; using GozareshgirProgramManager.Application.Modules.Projects.DTOs;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimeProject; 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 class SetTimeSectionTime
{ {
public string Description { get; set; } public string Description { get; set; }
public int Hours { get; set; } public int Hours { get; set; }
public int Minutes { get; set; }
public TaskSectionAdditionalTimeType Type { get; set; }
} }

View File

@@ -4,8 +4,13 @@ using GozareshgirProgramManager.Application.Modules.Projects.DTOs;
using GozareshgirProgramManager.Domain._Common; using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain._Common.Exceptions; using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Phase;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories; using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
using GozareshgirProgramManager.Domain.SkillAgg.Repositories;
using GozareshgirProgramManager.Domain.UserAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimeProject; namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimeProject;
@@ -15,21 +20,33 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
private readonly IProjectPhaseRepository _projectPhaseRepository; private readonly IProjectPhaseRepository _projectPhaseRepository;
private readonly IProjectTaskRepository _projectTaskRepository; private readonly IProjectTaskRepository _projectTaskRepository;
private readonly IUnitOfWork _unitOfWork; 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 long? _userId;
private readonly ITaskSectionRepository _taskSectionRepository;
public SetTimeProjectCommandHandler( public SetTimeProjectCommandHandler(
IProjectRepository projectRepository, IProjectRepository projectRepository,
IProjectPhaseRepository projectPhaseRepository, IProjectPhaseRepository projectPhaseRepository,
IProjectTaskRepository projectTaskRepository, IProjectTaskRepository projectTaskRepository,
IUnitOfWork unitOfWork, IAuthHelper authHelper) IUnitOfWork unitOfWork, IAuthHelper authHelper,
IUserRepository userRepository, ISkillRepository skillRepository,
IPhaseSectionRepository phaseSectionRepository,
IProjectSectionRepository projectSectionRepository,
ITaskSectionRepository taskSectionRepository)
{ {
_projectRepository = projectRepository; _projectRepository = projectRepository;
_projectPhaseRepository = projectPhaseRepository; _projectPhaseRepository = projectPhaseRepository;
_projectTaskRepository = projectTaskRepository; _projectTaskRepository = projectTaskRepository;
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_authHelper = authHelper; _userRepository = userRepository;
_skillRepository = skillRepository;
_phaseSectionRepository = phaseSectionRepository;
_projectSectionRepository = projectSectionRepository;
_taskSectionRepository = taskSectionRepository;
_userId = authHelper.GetCurrentUserId(); _userId = authHelper.GetCurrentUserId();
} }
@@ -37,6 +54,10 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
{ {
switch (request.Level) switch (request.Level)
{ {
case ProjectHierarchyLevel.Project:
return await AssignProject(request);
case ProjectHierarchyLevel.Phase:
return await AssignProjectPhase(request);
case ProjectHierarchyLevel.Task: case ProjectHierarchyLevel.Task:
return await SetTimeForProjectTask(request, cancellationToken); return await SetTimeForProjectTask(request, cancellationToken);
default: default:
@@ -44,67 +65,229 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
} }
} }
private async Task<OperationResult> SetTimeForProject(SetTimeProjectCommand request, private async Task<OperationResult> AssignProject(SetTimeProjectCommand request)
CancellationToken cancellationToken)
{ {
var project = await _projectRepository.GetWithFullHierarchyAsync(request.Id); var project = await _projectRepository.GetWithFullHierarchyAsync(request.Id);
if (project == null) if (project is null)
{ {
return OperationResult.NotFound("پروژه یافت نشد"); 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 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); phase.RemovePhaseSection(section.SkillId);
if (sectionItem != null) }
// برای 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(); return OperationResult.Success();
} }
private async Task<OperationResult> SetTimeForProjectPhase(SetTimeProjectCommand request, private async Task<OperationResult> AssignProjectPhase(SetTimeProjectCommand request)
CancellationToken cancellationToken)
{ {
var phase = await _projectPhaseRepository.GetWithTasksAsync(request.Id); var phase = await _projectPhaseRepository.GetWithTasksAsync(request.Id);
if (phase == null) if (phase is null)
{ {
return OperationResult.NotFound("فاز پروژه یافت نشد"); 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 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); // حذف TaskSections که در validSkills نیستند
if (sectionItem != null) 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(); return OperationResult.Success();
} }
private async Task<OperationResult> SetTimeForProjectTask(SetTimeProjectCommand request, private async Task<OperationResult> SetTimeForProjectTask(SetTimeProjectCommand request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@@ -116,24 +299,64 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
long? addedByUserId = _userId; long? addedByUserId = _userId;
// تنظیم زمان مستقیماً برای sections این تسک var validSkills = request.SkillItems
foreach (var section in task.Sections) .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); task.RemoveSection(sectionToRemove.Id);
if (sectionItem != null)
{
SetSectionTime(section, sectionItem, addedByUserId);
}
} }
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); await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success(); 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 initData = sectionItem.InitData;
var initialTime = TimeSpan.FromHours(initData.Hours); var initialTime = TimeSpan.FromHours(initData.Hours)
.Add(TimeSpan.FromMinutes(initData.Minutes));
if (initialTime <= TimeSpan.Zero) if (initialTime <= TimeSpan.Zero)
{ {
@@ -145,10 +368,26 @@ public class SetTimeProjectCommandHandler : IBaseCommandHandler<SetTimeProjectCo
section.ClearAdditionalTimes(); section.ClearAdditionalTimes();
// افزودن زمان‌های اضافی // افزودن زمان‌های اضافی
bool hasAdditionalTime = false;
foreach (var additionalTime in sectionItem.AdditionalTime) 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); section.AddAdditionalTime(additionalTimeSpan, additionalTime.Type, additionalTime.Description, addedByUserId);
hasAdditionalTime = true;
}
// تغییر status به Incomplete فقط اگر زمان اضافی اضافه شده باشد و در وضعیتی غیر از ReadyToStart باشد
if (hasAdditionalTime && section.Status != TaskSectionStatus.ReadyToStart)
{
// اگر سکشن درحال انجام است، باید متوقف شود قبل از تغییر status
if (section.Status == TaskSectionStatus.InProgress)
{
section.StopWork(TaskSectionStatus.Incomplete);
}
else
{
section.UpdateStatus(TaskSectionStatus.Incomplete);
}
} }
} }

View File

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

View File

@@ -89,6 +89,7 @@ public class ProjectSectionDto
public TimeSpan FinalEstimatedHours { get; set; } public TimeSpan FinalEstimatedHours { get; set; }
public TimeSpan TotalTimeSpent { get; set; } public TimeSpan TotalTimeSpent { get; set; }
public double ProgressPercentage { get; set; }
public bool IsCompleted { get; set; } public bool IsCompleted { get; set; }
public bool IsInProgress { get; set; } public bool IsInProgress { get; set; }

View File

@@ -2,9 +2,10 @@ using GozareshgirProgramManager.Application.Modules.Projects.Commands.SetTimePro
namespace GozareshgirProgramManager.Application.Modules.Projects.DTOs; 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 SetTimeSectionTime InitData { get; set; }
public List<SetTimeSectionTime> AdditionalTime { get; set; } = []; public List<SetTimeSectionTime> AdditionalTime { get; set; } = [];
} }

View File

@@ -1,5 +1,9 @@
using GozareshgirProgramManager.Application.Modules.Projects.DTOs; using GozareshgirProgramManager.Application.Modules.Projects.DTOs;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Phase;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Extensions; namespace GozareshgirProgramManager.Application.Modules.Projects.Extensions;
@@ -166,6 +170,7 @@ public static class ProjectMappingExtensions
CreationDate = section.CreationDate, CreationDate = section.CreationDate,
FinalEstimatedHours = section.FinalEstimatedHours, FinalEstimatedHours = section.FinalEstimatedHours,
TotalTimeSpent = section.GetTotalTimeSpent(), TotalTimeSpent = section.GetTotalTimeSpent(),
ProgressPercentage = section.GetProgressPercentage(),
IsCompleted = section.IsCompleted(), IsCompleted = section.IsCompleted(),
IsInProgress = section.IsInProgress(), IsInProgress = section.IsInProgress(),
Activities = section.Activities.Select(a => a.ToDto()).ToList(), Activities = section.Activities.Select(a => a.ToDto()).ToList(),
@@ -188,6 +193,7 @@ public static class ProjectMappingExtensions
CreationDate = section.CreationDate, CreationDate = section.CreationDate,
FinalEstimatedHours = section.FinalEstimatedHours, FinalEstimatedHours = section.FinalEstimatedHours,
TotalTimeSpent = section.GetTotalTimeSpent(), TotalTimeSpent = section.GetTotalTimeSpent(),
ProgressPercentage = section.GetProgressPercentage(),
IsCompleted = section.IsCompleted(), IsCompleted = section.IsCompleted(),
IsInProgress = section.IsInProgress() IsInProgress = section.IsInProgress()
// No activities or additional times for summary // No activities or additional times for summary

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

@@ -1,18 +1,28 @@
using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList; namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
public record GetProjectListDto
// Base DTO shared across project, phase, and task
public class GetProjectItemDto
{ {
public Guid Id { get; init; } public Guid Id { get; init; }
public string Name { get; init; } = string.Empty; public string Name { get; init; } = string.Empty;
public int Percentage { get; init; } public int Percentage { get; init; }
public ProjectHierarchyLevel Level { get; init; } public ProjectHierarchyLevel Level { get; init; }
public Guid? ParentId { get; init; } public Guid? ParentId { get; init; }
public bool HasFront { get; set; } public int TotalHours { get; set; }
public bool HasBackend { get; set; } public int Minutes { get; set; }
public bool HasDesign { get; set; } public AssignmentStatus Front { get; set; }
public AssignmentStatus Backend { get; set; }
public AssignmentStatus Design { get; set; }
} }
// Project DTO (no extra fields; inherits from base)
public class GetProjectDto : GetProjectItemDto
{
}
// Phase DTO (no extra fields; inherits from base)
public class GetPhaseDto : GetProjectItemDto
{
}

View File

@@ -3,6 +3,11 @@ using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Linq;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Phase;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList; namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
@@ -17,278 +22,343 @@ public class GetProjectsListQueryHandler : IBaseQueryHandler<GetProjectsListQuer
public async Task<OperationResult<GetProjectsListResponse>> Handle(GetProjectsListQuery request, CancellationToken cancellationToken) public async Task<OperationResult<GetProjectsListResponse>> Handle(GetProjectsListQuery request, CancellationToken cancellationToken)
{ {
List<GetProjectListDto> projects; var projects = new List<GetProjectDto>();
var phases = new List<GetPhaseDto>();
var tasks = new List<GetTaskDto>();
switch (request.HierarchyLevel) switch (request.HierarchyLevel)
{ {
case ProjectHierarchyLevel.Project: case ProjectHierarchyLevel.Project:
projects = await GetProjects(request.ParentId, cancellationToken); projects = await GetProjects(request.ParentId, cancellationToken);
await SetSkillFlags(projects, cancellationToken);
break; break;
case ProjectHierarchyLevel.Phase: case ProjectHierarchyLevel.Phase:
projects = await GetPhases(request.ParentId, cancellationToken); phases = await GetPhases(request.ParentId, cancellationToken);
await SetSkillFlags(phases, cancellationToken);
break; break;
case ProjectHierarchyLevel.Task: case ProjectHierarchyLevel.Task:
projects = await GetTasks(request.ParentId, cancellationToken); tasks = await GetTasks(request.ParentId, cancellationToken);
// Tasks don't need SetSkillFlags because they have Sections list
break; break;
default: default:
return OperationResult<GetProjectsListResponse>.Failure("سطح سلسله مراتب نامعتبر است"); return OperationResult<GetProjectsListResponse>.Failure("سطح سلسله مراتب نامعتبر است");
} }
await SetSkillFlags(projects, cancellationToken);
var response = new GetProjectsListResponse(projects); var response = new GetProjectsListResponse(projects, phases, tasks);
return OperationResult<GetProjectsListResponse>.Success(response); return OperationResult<GetProjectsListResponse>.Success(response);
} }
private async Task<List<GetProjectListDto>> GetProjects(Guid? parentId, CancellationToken cancellationToken) private async Task<List<GetProjectDto>> GetProjects(Guid? parentId, CancellationToken cancellationToken)
{ {
var query = _context.Projects.AsQueryable(); var query = _context.Projects.AsQueryable();
// پروژه‌ها سطح بالا هستند و parentId ندارند، فقط در صورت null بودن parentId نمایش داده می‌شوند
if (parentId.HasValue) if (parentId.HasValue)
{ {
return new List<GetProjectListDto>(); // پروژه‌ها parent ندارند return new List<GetProjectDto>();
} }
var entities = await query
var projects = await query
.OrderByDescending(p => p.CreationDate) .OrderByDescending(p => p.CreationDate)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var result = new List<GetProjectListDto>(); var result = new List<GetProjectDto>();
foreach (var project in entities)
foreach (var project in projects)
{ {
var percentage = await CalculateProjectPercentage(project, cancellationToken); var (percentage, totalTime) = await CalculateProjectPercentage(project, cancellationToken);
result.Add(new GetProjectListDto result.Add(new GetProjectDto
{ {
Id = project.Id, Id = project.Id,
Name = project.Name, Name = project.Name,
Level = ProjectHierarchyLevel.Project, Level = ProjectHierarchyLevel.Project,
ParentId = null, ParentId = null,
Percentage = percentage Percentage = percentage,
TotalHours = (int)totalTime.TotalHours,
Minutes = totalTime.Minutes,
}); });
} }
return result; return result;
} }
private async Task<List<GetProjectListDto>> GetPhases(Guid? parentId, CancellationToken cancellationToken) private async Task<List<GetPhaseDto>> GetPhases(Guid? parentId, CancellationToken cancellationToken)
{ {
var query = _context.ProjectPhases.AsQueryable(); var query = _context.ProjectPhases.AsQueryable();
if (parentId.HasValue) if (parentId.HasValue)
{ {
query = query.Where(x => x.ProjectId == parentId); query = query.Where(x => x.ProjectId == parentId);
} }
var entities = await query
var phases = await query
.OrderByDescending(p => p.CreationDate) .OrderByDescending(p => p.CreationDate)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var result = new List<GetProjectListDto>(); var result = new List<GetPhaseDto>();
foreach (var phase in entities)
foreach (var phase in phases)
{ {
var percentage = await CalculatePhasePercentage(phase, cancellationToken); var (percentage, totalTime) = await CalculatePhasePercentage(phase, cancellationToken);
result.Add(new GetProjectListDto result.Add(new GetPhaseDto
{ {
Id = phase.Id, Id = phase.Id,
Name = phase.Name, Name = phase.Name,
Level = ProjectHierarchyLevel.Phase, Level = ProjectHierarchyLevel.Phase,
ParentId = phase.ProjectId, ParentId = phase.ProjectId,
Percentage = percentage Percentage = percentage,
TotalHours = (int)totalTime.TotalHours,
Minutes = totalTime.Minutes,
}); });
} }
return result; return result;
} }
private async Task<List<GetProjectListDto>> GetTasks(Guid? parentId, CancellationToken cancellationToken) private async Task<List<GetTaskDto>> GetTasks(Guid? parentId, CancellationToken cancellationToken)
{ {
var query = _context.ProjectTasks.AsQueryable(); var query = _context.ProjectTasks.AsQueryable();
if (parentId.HasValue) if (parentId.HasValue)
{ {
query = query.Where(x => x.PhaseId == parentId); query = query.Where(x => x.PhaseId == parentId);
} }
var entities = await query
var tasks = await query
.OrderByDescending(t => t.CreationDate) .OrderByDescending(t => t.CreationDate)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var result = new List<GetProjectListDto>(); var result = new List<GetTaskDto>();
// دریافت تمام Skills
var allSkills = await _context.Skills
.Select(s => new { s.Id, s.Name })
.ToListAsync(cancellationToken);
foreach (var task in tasks) foreach (var task in entities)
{ {
var percentage = await CalculateTaskPercentage(task, cancellationToken); var (percentage, totalTime) = await CalculateTaskPercentage(task, cancellationToken);
result.Add(new GetProjectListDto var sections = await _context.TaskSections
.Include(s => s.Activities)
.Include(s => s.Skill)
.Where(s => s.TaskId == task.Id)
.ToListAsync(cancellationToken);
// جمع‌آوری تمام userId های مورد نیاز
var userIds = sections
.Where(s => s.CurrentAssignedUserId > 0)
.Select(s => s.CurrentAssignedUserId)
.Distinct()
.ToList();
// دریافت اطلاعات کاربران
var users = await _context.Users
.Where(u => userIds.Contains(u.Id))
.Select(u => new { u.Id, u.FullName })
.ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
// محاسبه SpentTime و RemainingTime
var spentTime = TimeSpan.FromTicks(sections.Sum(s => s.Activities.Sum(a => a.GetTimeSpent().Ticks)));
var remainingTime = totalTime - spentTime;
// ساخت section DTOs برای تمام Skills
var sectionDtos = allSkills.Select(skill =>
{
var section = sections.FirstOrDefault(s => s.SkillId == skill.Id);
if (section == null)
{
// اگر section وجود نداشت، یک DTO با وضعیت Unassigned برمی‌گردانیم
return new GetTaskSectionDto
{
Id = Guid.Empty,
SkillName = skill.Name ?? string.Empty,
SpentTime = TimeSpan.Zero,
TotalTime = TimeSpan.Zero,
Percentage = 0,
UserFullName = string.Empty,
AssignmentStatus = AssignmentStatus.Unassigned
};
}
// اگر section وجود داشت
return new GetTaskSectionDto
{
Id = section.Id,
SkillName = skill.Name ?? string.Empty,
SpentTime = TimeSpan.FromTicks(section.Activities.Sum(a => a.GetTimeSpent().Ticks)),
TotalTime = section.FinalEstimatedHours,
Percentage = (int)section.GetProgressPercentage(),
UserFullName = section.CurrentAssignedUserId > 0 && users.ContainsKey(section.CurrentAssignedUserId)
? users[section.CurrentAssignedUserId]
: string.Empty,
AssignmentStatus = GetAssignmentStatus(section)
};
}).ToList();
result.Add(new GetTaskDto
{ {
Id = task.Id, Id = task.Id,
Name = task.Name, Name = task.Name,
Level = ProjectHierarchyLevel.Task, Level = ProjectHierarchyLevel.Task,
ParentId = task.PhaseId, ParentId = task.PhaseId,
Percentage = percentage Percentage = percentage,
TotalHours = (int)totalTime.TotalHours,
Minutes = totalTime.Minutes,
SpentTime = spentTime,
RemainingTime = remainingTime,
Sections = sectionDtos,
Priority = task.Priority
}); });
} }
return result; return result;
} }
private async Task SetSkillFlags(List<GetProjectListDto> projects, CancellationToken cancellationToken) private async Task SetSkillFlags<TItem>(List<TItem> items, CancellationToken cancellationToken) where TItem : GetProjectItemDto
{ {
if (!projects.Any()) if (!items.Any())
return; return;
var ids = items.Select(x => x.Id).ToList();
var projectIds = projects.Select(x => x.Id).ToList(); var hierarchyLevel = items.First().Level;
var hierarchyLevel = projects.First().Level;
switch (hierarchyLevel) switch (hierarchyLevel)
{ {
case ProjectHierarchyLevel.Project: case ProjectHierarchyLevel.Project:
await SetSkillFlagsForProjects(projects, projectIds, cancellationToken); await SetSkillFlagsForProjects(items, ids, cancellationToken);
break; break;
case ProjectHierarchyLevel.Phase: case ProjectHierarchyLevel.Phase:
await SetSkillFlagsForPhases(projects, projectIds, cancellationToken); await SetSkillFlagsForPhases(items, ids, cancellationToken);
break;
case ProjectHierarchyLevel.Task:
await SetSkillFlagsForTasks(projects, projectIds, cancellationToken);
break; break;
} }
} }
private async Task SetSkillFlagsForProjects(List<GetProjectListDto> projects, List<Guid> projectIds, CancellationToken cancellationToken)
private async Task SetSkillFlagsForProjects<TItem>(List<TItem> items, List<Guid> projectIds, CancellationToken cancellationToken) where TItem : GetProjectItemDto
{ {
var projectSections = await _context.ProjectSections // For projects: gather all phases, then tasks, then sections
.Include(x => x.Skill) var phases = await _context.ProjectPhases
.Where(s => projectIds.Contains(s.ProjectId)) .Where(ph => projectIds.Contains(ph.ProjectId))
.Select(ph => ph.Id)
.ToListAsync(cancellationToken);
var tasks = await _context.ProjectTasks
.Where(t => phases.Contains(t.PhaseId))
.Select(t => t.Id)
.ToListAsync(cancellationToken);
var sections = await _context.TaskSections
.Include(s => s.Skill)
.Where(s => tasks.Contains(s.TaskId))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (!projectSections.Any()) foreach (var item in items)
return;
foreach (var project in projects)
{ {
var sections = projectSections.Where(s => s.ProjectId == project.Id).ToList(); var relatedPhases = phases; // used for filtering tasks by project
project.HasBackend = sections.Any(x => x.Skill?.Name == "Backend"); var relatedTasks = await _context.ProjectTasks
project.HasFront = sections.Any(x => x.Skill?.Name == "Frontend"); .Where(t => t.PhaseId != Guid.Empty && relatedPhases.Contains(t.PhaseId))
project.HasDesign = sections.Any(x => x.Skill?.Name == "UI/UX Design"); .Select(t => t.Id)
.ToListAsync(cancellationToken);
var itemSections = sections.Where(s => relatedTasks.Contains(s.TaskId));
item.Backend = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "Backend"));
item.Front = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "Frontend"));
item.Design = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "UI/UX Design"));
} }
} }
private async Task SetSkillFlagsForPhases(List<GetProjectListDto> projects, List<Guid> phaseIds, CancellationToken cancellationToken) private async Task SetSkillFlagsForPhases<TItem>(List<TItem> items, List<Guid> phaseIds, CancellationToken cancellationToken) where TItem : GetProjectItemDto
{ {
var phaseSections = await _context.PhaseSections // For phases: gather tasks, then sections
.Include(x => x.Skill) var tasks = await _context.ProjectTasks
.Where(s => phaseIds.Contains(s.PhaseId)) .Where(t => phaseIds.Contains(t.PhaseId))
.Select(t => t.Id)
.ToListAsync(cancellationToken);
var sections = await _context.TaskSections
.Include(s => s.Skill)
.Where(s => tasks.Contains(s.TaskId))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (!phaseSections.Any()) foreach (var item in items)
return;
foreach (var phase in projects)
{ {
var sections = phaseSections.Where(s => s.PhaseId == phase.Id).ToList(); // Filter tasks for this phase
phase.HasBackend = sections.Any(x => x.Skill?.Name == "Backend"); var phaseTaskIds = await _context.ProjectTasks
phase.HasFront = sections.Any(x => x.Skill?.Name == "Frontend"); .Where(t => t.PhaseId == item.Id)
phase.HasDesign = sections.Any(x => x.Skill?.Name == "UI/UX Design"); .Select(t => t.Id)
.ToListAsync(cancellationToken);
var itemSections = sections.Where(s => phaseTaskIds.Contains(s.TaskId));
item.Backend = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "Backend"));
item.Front = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "Frontend"));
item.Design = GetAssignmentStatus(itemSections.FirstOrDefault(x => x.Skill?.Name == "UI/UX Design"));
} }
} }
private async Task SetSkillFlagsForTasks(List<GetProjectListDto> projects, List<Guid> taskIds, CancellationToken cancellationToken) private async Task<(int Percentage, TimeSpan TotalTime)> CalculateProjectPercentage(Project project, CancellationToken cancellationToken)
{ {
var taskSections = await _context.TaskSections
.Include(x => x.Skill)
.Where(s => taskIds.Contains(s.TaskId))
.ToListAsync(cancellationToken);
if (!taskSections.Any())
return;
foreach (var task in projects)
{
var sections = taskSections.Where(s => s.TaskId == task.Id).ToList();
task.HasBackend = sections.Any(x => x.Skill?.Name == "Backend");
task.HasFront = sections.Any(x => x.Skill?.Name == "Frontend");
task.HasDesign = sections.Any(x => x.Skill?.Name == "UI/UX Design");
}
}
private async Task<int> CalculateProjectPercentage(Project project, CancellationToken cancellationToken)
{
// گرفتن تمام فازهای پروژه
var phases = await _context.ProjectPhases var phases = await _context.ProjectPhases
.Where(ph => ph.ProjectId == project.Id) .Where(ph => ph.ProjectId == project.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (!phases.Any()) if (!phases.Any())
return 0; return (0, TimeSpan.Zero);
// محاسبه درصد هر فاز و میانگین‌گیری
var phasePercentages = new List<int>(); var phasePercentages = new List<int>();
var totalTime = TimeSpan.Zero;
foreach (var phase in phases) foreach (var phase in phases)
{ {
var phasePercentage = await CalculatePhasePercentage(phase, cancellationToken); var (phasePercentage, phaseTime) = await CalculatePhasePercentage(phase, cancellationToken);
phasePercentages.Add(phasePercentage); phasePercentages.Add(phasePercentage);
totalTime += phaseTime;
} }
var averagePercentage = phasePercentages.Any() ? (int)phasePercentages.Average() : 0;
return 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 var tasks = await _context.ProjectTasks
.Where(t => t.PhaseId == phase.Id) .Where(t => t.PhaseId == phase.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (!tasks.Any()) if (!tasks.Any())
return 0; return (0, TimeSpan.Zero);
// محاسبه درصد هر تسک و میانگین‌گیری
var taskPercentages = new List<int>(); var taskPercentages = new List<int>();
var totalTime = TimeSpan.Zero;
foreach (var task in tasks) foreach (var task in tasks)
{ {
var taskPercentage = await CalculateTaskPercentage(task, cancellationToken); var (taskPercentage, taskTime) = await CalculateTaskPercentage(task, cancellationToken);
taskPercentages.Add(taskPercentage); taskPercentages.Add(taskPercentage);
totalTime += taskTime;
} }
var averagePercentage = taskPercentages.Any() ? (int)taskPercentages.Average() : 0;
return 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 var sections = await _context.TaskSections
.Include(s => s.Activities) .Include(s => s.Activities)
.Include(x=>x.AdditionalTimes)
.Where(s => s.TaskId == task.Id) .Where(s => s.TaskId == task.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (!sections.Any()) if (!sections.Any())
return 0; return (0, TimeSpan.Zero);
// محاسبه درصد هر سکشن و میانگین‌گیری
var sectionPercentages = new List<int>(); var sectionPercentages = new List<int>();
var totalTime = TimeSpan.Zero;
foreach (var section in sections) foreach (var section in sections)
{ {
var sectionPercentage = CalculateSectionPercentage(section); var (sectionPercentage, sectionTime) = CalculateSectionPercentage(section);
sectionPercentages.Add(sectionPercentage); sectionPercentages.Add(sectionPercentage);
totalTime += sectionTime;
} }
var averagePercentage = sectionPercentages.Any() ? (int)sectionPercentages.Average() : 0;
return sectionPercentages.Any() ? (int)sectionPercentages.Average() : 0; return (averagePercentage, totalTime);
} }
private static int CalculateSectionPercentage(TaskSection section) private static (int Percentage, TimeSpan TotalTime) CalculateSectionPercentage(TaskSection section)
{ {
// محاسبه کل زمان تخمین زده شده (اولیه + اضافی) return ((int)section.GetProgressPercentage(),section.FinalEstimatedHours);
var totalEstimatedHours = section.FinalEstimatedHours.TotalHours; }
if (totalEstimatedHours <= 0) private static AssignmentStatus GetAssignmentStatus(TaskSection? section)
return 0; {
// تعیین تکلیف نشده: section وجود ندارد
if (section == null)
return AssignmentStatus.Unassigned;
// محاسبه کل زمان صرف شده از activities // بررسی وجود user
var totalSpentHours = section.Activities.Sum(a => a.GetTimeSpent().TotalHours); bool hasUser = section.CurrentAssignedUserId > 0;
// بررسی وجود time (InitialEstimatedHours بزرگتر از صفر باشد)
bool hasTime = section.InitialEstimatedHours > TimeSpan.Zero;
// محاسبه درصد (حداکثر 100%) // تعیین تکلیف شده: هم user و هم time تعیین شده
var percentage = (totalSpentHours / totalEstimatedHours) * 100; if (hasUser && hasTime)
return Math.Min((int)Math.Round(percentage), 100); return AssignmentStatus.Assigned;
// فقط کاربر تعیین شده: user دارد ولی time ندارد
if (hasUser && !hasTime)
return AssignmentStatus.UserOnly;
// تعیین تکلیف نشده: نه user دارد نه time
return AssignmentStatus.Unassigned;
} }
} }

View File

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

View File

@@ -0,0 +1,32 @@
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
public class GetTaskDto
{
public Guid Id { get; init; }
public string Name { get; init; } = string.Empty;
public int Percentage { get; init; }
public ProjectHierarchyLevel Level { get; init; }
public Guid? ParentId { get; init; }
public int TotalHours { get; set; }
public int Minutes { get; set; }
// Task-specific fields
public TimeSpan SpentTime { get; init; }
public TimeSpan RemainingTime { get; init; }
public ProjectTaskPriority Priority { get; set; }
public List<GetTaskSectionDto> Sections { get; init; }
}
public class GetTaskSectionDto
{
public Guid Id { get; init; }
public string SkillName { get; init; } = string.Empty;
public TimeSpan SpentTime { get; init; }
public TimeSpan TotalTime { get; init; }
public int Percentage { get; init; }
public string UserFullName{ get; init; } = string.Empty;
public AssignmentStatus AssignmentStatus { get; set; }
}

View File

@@ -6,5 +6,7 @@ namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.Project
public record ProjectBoardListQuery: IBaseQuery<List<ProjectBoardListResponse>> public record ProjectBoardListQuery: IBaseQuery<List<ProjectBoardListResponse>>
{ {
public long? UserId { get; set; }
public string? SearchText { get; set; }
public TaskSectionStatus? Status { get; set; } public TaskSectionStatus? Status { get; set; }
} }

View File

@@ -3,7 +3,6 @@ using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models; using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query.Internal;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectBoardList; namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectBoardList;
@@ -24,7 +23,8 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
var currentUserId = _authHelper.GetCurrentUserId(); var currentUserId = _authHelper.GetCurrentUserId();
var queryable = _programManagerDbContext.TaskSections.AsNoTracking() var queryable = _programManagerDbContext.TaskSections.AsNoTracking()
.Where(x => x.InitialEstimatedHours > TimeSpan.Zero && x.Status != TaskSectionStatus.Completed) .Where(x => x.InitialEstimatedHours > TimeSpan.Zero
&& x.Status != TaskSectionStatus.Completed)
.Include(x => x.Task) .Include(x => x.Task)
.ThenInclude(x => x.Phase) .ThenInclude(x => x.Phase)
.ThenInclude(x => x.Project) .ThenInclude(x => x.Project)
@@ -40,10 +40,23 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
{ {
queryable = queryable.Where(x => x.Status == request.Status); queryable = queryable.Where(x => x.Status == request.Status);
} }
if (request.UserId is > 0)
{
queryable = queryable.Where(x => x.CurrentAssignedUserId == request.UserId);
}
if (!string.IsNullOrWhiteSpace(request.SearchText))
{
queryable = queryable.Where(x=>x.Task.Name.Contains(request.SearchText)
|| x.Task.Phase.Name.Contains(request.SearchText)
|| x.Task.Phase.Project.Name.Contains(request.SearchText));
}
var data = await queryable.ToListAsync(cancellationToken); var data = await queryable.ToListAsync(cancellationToken);
var activityUserIds = data.SelectMany(x => x.Activities).Select(a => a.UserId).Distinct().ToList(); var activityUserIds = data.SelectMany(x => x.Activities)
.Select(a => a.UserId).Distinct().ToList();
var assignedUser = data.Select(x => x.CurrentAssignedUserId) var assignedUser = data.Select(x => x.CurrentAssignedUserId)
.Concat(data.Select(x => x.OriginalAssignedUserId)).ToList(); .Concat(data.Select(x => x.OriginalAssignedUserId)).ToList();
var allUserIds = activityUserIds.Concat(assignedUser).Distinct().ToList(); var allUserIds = activityUserIds.Concat(assignedUser).Distinct().ToList();
@@ -53,68 +66,87 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
.ToDictionaryAsync(x => x.Id, x => x.FullName, cancellationToken); .ToDictionaryAsync(x => x.Id, x => x.FullName, cancellationToken);
var result = data.Select(x => var result = data
{ .OrderByDescending(x => x.CurrentAssignedUserId == currentUserId)
// محاسبه یکبار برای هر Activity و Cache کردن نتیجه .ThenByDescending(x=>x.Task.Priority)
var activityTimeData = x.Activities.Select(a => .ThenBy(x => GetStatusOrder(x.Status))
.Select(x =>
{ {
var timeSpent = a.GetTimeSpent(); // محاسبه یکبار برای هر Activity و Cache کردن نتیجه
return new var activityTimeData = x.Activities.Select(a =>
{ {
Activity = a, var timeSpent = a.GetTimeSpent();
TimeSpent = timeSpent, return new
TotalSeconds = timeSpent.TotalSeconds, {
FormattedTime = timeSpent.ToString(@"hh\:mm") Activity = a,
TimeSpent = timeSpent,
timeSpent.TotalSeconds,
FormattedTime = timeSpent.ToString(@"hh\:mm")
};
}).ToList();
// ادغام پشت سر هم فعالیت‌های یک کاربر
var mergedHistories = new List<ProjectProgressHistoryDto>();
foreach (var activityData in activityTimeData)
{
var lastHistory = mergedHistories.LastOrDefault();
// اگر آخرین history برای همین کاربر باشد، زمان‌ها را جمع می‌کنیم
if (lastHistory != null && lastHistory.UserId == activityData.Activity.UserId)
{
var totalTimeSpan = lastHistory.WorkedTimeSpan + activityData.TimeSpent;
lastHistory.WorkedTimeSpan = totalTimeSpan;
lastHistory.WorkedTime = totalTimeSpan.ToString(@"hh\:mm");
}
else
{
// در غیر این صورت، یک history جدید اضافه می‌کنیم
mergedHistories.Add(new ProjectProgressHistoryDto()
{
UserId = activityData.Activity.UserId,
IsCurrentUser = activityData.Activity.UserId == currentUserId,
Name = users.GetValueOrDefault(activityData.Activity.UserId, "ناشناس"),
WorkedTime = activityData.FormattedTime,
WorkedTimeSpan = activityData.TimeSpent,
});
}
}
return new ProjectBoardListResponse()
{
Id = x.Id,
PhaseName = x.Task.Phase.Name,
ProjectName = x.Task.Phase.Project.Name,
TaskName = x.Task.Name,
SectionStatus = x.Status,
TaskPriority = x.Task.Priority,
Progress = new ProjectProgressDto()
{
CompleteSecond = x.FinalEstimatedHours.TotalSeconds,
Percentage = (int)x.GetProgressPercentage(),
CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds),
Histories = mergedHistories
},
OriginalUser = users.GetValueOrDefault(x.OriginalAssignedUserId, "ناشناس"),
AssignedUser = x.CurrentAssignedUserId == x.OriginalAssignedUserId ? null
: users.GetValueOrDefault(x.CurrentAssignedUserId, "ناشناس"),
SkillName = x.Skill?.Name??"-",
TaskId = x.TaskId
}; };
}).ToList(); }).ToList();
// ادغام پشت سر هم فعالیت‌های یک کاربر
var mergedHistories = new List<ProjectProgressHistoryDto>();
foreach (var activityData in activityTimeData)
{
var lastHistory = mergedHistories.LastOrDefault();
// اگر آخرین history برای همین کاربر باشد، زمان‌ها را جمع می‌کنیم
if (lastHistory != null && lastHistory.UserId == activityData.Activity.UserId)
{
var totalTimeSpan = lastHistory.WorkedTimeSpan + activityData.TimeSpent;
lastHistory.WorkedTimeSpan = totalTimeSpan;
lastHistory.WorkedTime = totalTimeSpan.ToString(@"hh\:mm");
}
else
{
// در غیر این صورت، یک history جدید اضافه می‌کنیم
mergedHistories.Add(new ProjectProgressHistoryDto()
{
UserId = activityData.Activity.UserId,
IsCurrentUser = activityData.Activity.UserId == currentUserId,
Name = users.GetValueOrDefault(activityData.Activity.UserId, "ناشناس"),
WorkedTime = activityData.FormattedTime,
WorkedTimeSpan = activityData.TimeSpent,
});
}
}
return new ProjectBoardListResponse()
{
Id = x.Id,
PhaseName = x.Task.Phase.Name,
ProjectName = x.Task.Phase.Project.Name,
TaskName = x.Task.Name,
SectionStatus = x.Status,
Progress = new ProjectProgressDto()
{
CompleteSecond = x.FinalEstimatedHours.TotalSeconds,
CurrentSecond = activityTimeData.Sum(a => a.TotalSeconds),
Histories = mergedHistories
},
OriginalUser = users.GetValueOrDefault(x.OriginalAssignedUserId, "ناشناس"),
AssignedUser = x.CurrentAssignedUserId == x.OriginalAssignedUserId ? null
: users.GetValueOrDefault(x.CurrentAssignedUserId, "ناشناس"),
SkillName = x.Skill?.Name??"-",
};
}).ToList();
return OperationResult<List<ProjectBoardListResponse>>.Success(result); return OperationResult<List<ProjectBoardListResponse>>.Success(result);
} }
private static int GetStatusOrder(TaskSectionStatus status)
{
return status switch
{
TaskSectionStatus.InProgress => 0,
TaskSectionStatus.Incomplete => 1,
TaskSectionStatus.NotAssigned => 2,
TaskSectionStatus.ReadyToStart => 2,
TaskSectionStatus.PendingForCompletion => 3,
_ => 99
};
}
} }

View File

@@ -13,11 +13,14 @@ public class ProjectBoardListResponse
public string? AssignedUser { get; set; } public string? AssignedUser { get; set; }
public string OriginalUser { get; set; } public string OriginalUser { get; set; }
public string SkillName { get; set; } public string SkillName { get; set; }
public ProjectTaskPriority TaskPriority { get; set; }
public Guid TaskId { get; set; }
} }
public class ProjectProgressDto public class ProjectProgressDto
{ {
public double CurrentSecond { get; set; } public double CurrentSecond { get; set; }
public int Percentage { get; set; }
public double CompleteSecond { get; set; } public double CompleteSecond { get; set; }
public List<ProjectProgressHistoryDto> Histories { get; set; } public List<ProjectProgressHistoryDto> Histories { get; set; }
} }

View File

@@ -12,14 +12,16 @@ public record ProjectDeployBoardDetailsResponse(
public record ProjectDeployBoardDetailPhaseItem( public record ProjectDeployBoardDetailPhaseItem(
string Name, string Name,
TimeSpan TotalTimeSpan, TimeSpan TotalTimeSpan,
TimeSpan DoneTimeSpan); TimeSpan DoneTimeSpan,
int Percentage);
public record ProjectDeployBoardDetailTaskItem( public record ProjectDeployBoardDetailTaskItem(
string Name, string Name,
TimeSpan TotalTimeSpan, TimeSpan TotalTimeSpan,
TimeSpan DoneTimeSpan, TimeSpan DoneTimeSpan,
int Percentage,
List<ProjectDeployBoardDetailItemSkill> Skills) List<ProjectDeployBoardDetailItemSkill> Skills)
: ProjectDeployBoardDetailPhaseItem(Name, TotalTimeSpan, DoneTimeSpan); : ProjectDeployBoardDetailPhaseItem(Name, TotalTimeSpan, DoneTimeSpan, Percentage);
public record ProjectDeployBoardDetailItemSkill(string OriginalUserFullName, string SkillName, int TimePercentage); public record ProjectDeployBoardDetailItemSkill(string OriginalUserFullName, string SkillName, int TimePercentage);
@@ -71,6 +73,7 @@ public class
var doneTime = t.Sections.Aggregate(TimeSpan.Zero, var doneTime = t.Sections.Aggregate(TimeSpan.Zero,
(sum, next) => sum.Add(next.GetTotalTimeSpent())); (sum, next) => sum.Add(next.GetTotalTimeSpent()));
var skills = t.Sections var skills = t.Sections
.Select(s => .Select(s =>
{ {
@@ -79,22 +82,30 @@ public class
var skillName = s.Skill?.Name ?? "بدون مهارت"; var skillName = s.Skill?.Name ?? "بدون مهارت";
var totalTimeSpent = s.GetTotalTimeSpent(); var timePercentage = (int)s.GetProgressPercentage();
var timePercentage = s.FinalEstimatedHours.Ticks > 0
? (int)((totalTimeSpent.Ticks / (double)s.FinalEstimatedHours.Ticks) * 100)
: 0;
return new ProjectDeployBoardDetailItemSkill( return new ProjectDeployBoardDetailItemSkill(
originalUserFullName, originalUserFullName,
skillName, skillName,
timePercentage); timePercentage);
}).ToList(); }).ToList();
int taskPercentage;
if (skills.Count == 0)
{
taskPercentage = 0;
}
else
{
taskPercentage = (int)Math.Round(skills.Average(x => x.TimePercentage));
}
return new ProjectDeployBoardDetailTaskItem( return new ProjectDeployBoardDetailTaskItem(
t.Name, t.Name,
totalTime, totalTime,
doneTime, doneTime,
taskPercentage,
skills); skills);
}).ToList(); }).ToList();
@@ -104,7 +115,10 @@ public class
var doneTimeSpan = tasksRes.Aggregate(TimeSpan.Zero, var doneTimeSpan = tasksRes.Aggregate(TimeSpan.Zero,
(sum, next) => sum.Add(next.DoneTimeSpan)); (sum, next) => sum.Add(next.DoneTimeSpan));
var phaseRes = new ProjectDeployBoardDetailPhaseItem(phase.Name, totalTimeSpan, doneTimeSpan); var phasePercentage = tasksRes.Average(x => x.Percentage);
var phaseRes = new ProjectDeployBoardDetailPhaseItem(phase.Name, totalTimeSpan, doneTimeSpan,
(int)phasePercentage);
var res = new ProjectDeployBoardDetailsResponse(phaseRes, tasksRes); var res = new ProjectDeployBoardDetailsResponse(phaseRes, tasksRes);

View File

@@ -3,6 +3,7 @@ using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models; using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList; using GozareshgirProgramManager.Application.Modules.Projects.Queries.GetProjectsList;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities; using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Phase;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -17,6 +18,7 @@ public record ProjectDeployBoardListItem()
public int DoneTasks { get; set; } public int DoneTasks { get; set; }
public TimeSpan TotalTimeSpan { get; set; } public TimeSpan TotalTimeSpan { get; set; }
public TimeSpan DoneTimeSpan { get; set; } public TimeSpan DoneTimeSpan { get; set; }
public int Percentage { get; set; }
public ProjectDeployStatus DeployStatus { get; set; } public ProjectDeployStatus DeployStatus { get; set; }
} }
public record GetProjectsDeployBoardListResponse(List<ProjectDeployBoardListItem> Items); public record GetProjectsDeployBoardListResponse(List<ProjectDeployBoardListItem> Items);
@@ -66,7 +68,8 @@ public class ProjectDeployBoardListQueryHandler:IBaseQueryHandler<GetProjectDepl
.Select(x => x.TaskId).Distinct().Count(), .Select(x => x.TaskId).Distinct().Count(),
TotalTimeSpan = TimeSpan.FromTicks(g.Sum(x => x.InitialEstimatedHours.Ticks)), TotalTimeSpan = TimeSpan.FromTicks(g.Sum(x => x.InitialEstimatedHours.Ticks)),
DoneTimeSpan = TimeSpan.FromTicks(g.Sum(x=>x.GetTotalTimeSpent().Ticks)), DoneTimeSpan = TimeSpan.FromTicks(g.Sum(x=>x.GetTotalTimeSpent().Ticks)),
DeployStatus = g.First().Task.Phase.DeployStatus DeployStatus = g.First().Task.Phase.DeployStatus,
Percentage = (int)Math.Round(g.Average(x => x.GetProgressPercentage()))
}).ToList(); }).ToList();
var response = new GetProjectsDeployBoardListResponse(list); var response = new GetProjectsDeployBoardListResponse(list);
return OperationResult<GetProjectsDeployBoardListResponse>.Success(response); return OperationResult<GetProjectsDeployBoardListResponse>.Success(response);

View File

@@ -3,21 +3,22 @@ using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails; namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails;
public record ProjectSetTimeDetailsQuery(Guid TaskId) public record ProjectSetTimeDetailsQuery(Guid Id, ProjectHierarchyLevel Level)
: IBaseQuery<ProjectSetTimeResponse>; : IBaseQuery<ProjectSetTimeResponse>;
public record ProjectSetTimeResponse( public record ProjectSetTimeResponse(
List<ProjectSetTimeResponseSections> SectionItems, List<ProjectSetTimeResponseSkill> SkillItems,
Guid Id, Guid Id,
ProjectHierarchyLevel Level); ProjectHierarchyLevel Level);
public record ProjectSetTimeResponseSections public record ProjectSetTimeResponseSkill
{ {
public Guid SkillId { get; init; }
public string SkillName { get; init; } public string SkillName { get; init; }
public string UserName { get; init; } public long UserId { get; set; }
public int InitialTime { get; set; } public string UserFullName { get; init; }
public int InitialHours { get; set; }
public int InitialMinutes { get; set; }
public string InitialDescription { get; set; } public string InitialDescription { get; set; }
public int TotalEstimateTime { get; init; }
public int TotalAdditionalTime { get; init; }
public string InitCreationTime { get; init; } public string InitCreationTime { get; init; }
public List<ProjectSetTimeResponseSectionAdditionalTime> AdditionalTimes { get; init; } public List<ProjectSetTimeResponseSectionAdditionalTime> AdditionalTimes { get; init; }
public Guid SectionId { get; set; } public Guid SectionId { get; set; }
@@ -25,6 +26,8 @@ public record ProjectSetTimeResponseSections
public class ProjectSetTimeResponseSectionAdditionalTime 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 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, public async Task<OperationResult<ProjectSetTimeResponse>> Handle(ProjectSetTimeDetailsQuery request,
CancellationToken cancellationToken) 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 var task = await _context.ProjectTasks
.Where(p => p.Id == request.TaskId) .Where(p => p.Id == id)
.Include(x => x.Sections) .Include(x => x.Sections)
.ThenInclude(x => x.AdditionalTimes).AsNoTracking() .ThenInclude(x => x.AdditionalTimes).AsNoTracking()
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
if (task == null) if (task == null)
{ {
return OperationResult<ProjectSetTimeResponse>.NotFound("Project not found"); return OperationResult<ProjectSetTimeResponse>.NotFound("تسک یافت نشد");
} }
var userIds = task.Sections.Select(x => x.OriginalAssignedUserId) var userIds = task.Sections.Select(x => x.OriginalAssignedUserId)
.Distinct().ToList(); .Distinct().ToList();
@@ -40,40 +53,145 @@ public class ProjectSetTimeDetailsQueryHandler
.Where(x => userIds.Contains(x.Id)) .Where(x => userIds.Contains(x.Id))
.AsNoTracking() .AsNoTracking()
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var skillIds = task.Sections.Select(x => x.SkillId)
.Distinct().ToList();
var skills = await _context.Skills var skills = await _context.Skills
.Where(x => skillIds.Contains(x.Id))
.AsNoTracking() .AsNoTracking()
.OrderBy(x=>x.CreationDate)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var res = new ProjectSetTimeResponse( 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); AdditionalTimes = section?.AdditionalTimes
var skill = skills.FirstOrDefault(x => x.Id == ts.SkillId); .Select(x => new ProjectSetTimeResponseSectionAdditionalTime
return new ProjectSetTimeResponseSections {
{ Description = x.Reason ?? "",
AdditionalTimes = ts.AdditionalTimes Hours = (int)x.Hours.TotalHours,
.Select(x => new ProjectSetTimeResponseSectionAdditionalTime Minutes = x.Hours.Minutes,
{ CreationDate = x.CreationDate.ToFarsi()
Description = x.Reason ?? "", }).OrderBy(x => x.CreationDate).ToList() ?? [],
Time = (int)x.Hours.TotalHours InitCreationTime = section?.CreationDate.ToFarsi() ?? "",
}).ToList(), SkillName = skill.Name ?? "",
InitCreationTime = ts.CreationDate.ToFarsi(), UserFullName = user?.FullName ?? "",
SkillName = skill?.Name ?? "", SectionId = section?.Id ?? Guid.Empty,
TotalAdditionalTime = (int)ts.GetTotalAdditionalTime().TotalHours, InitialDescription = section?.InitialDescription ?? "",
TotalEstimateTime = (int)ts.FinalEstimatedHours.TotalHours, InitialHours = (int)(section?.InitialEstimatedHours.TotalHours ?? 0),
UserName = user?.UserName ?? "", InitialMinutes = section?.InitialEstimatedHours.Minutes ?? 0,
SectionId = ts.Id, UserId = section?.OriginalAssignedUserId ?? 0,
InitialDescription = ts.InitialDescription ?? "", SkillId = skill.Id,
InitialTime = (int)ts.InitialEstimatedHours.TotalHours };
}; }).ToList(),
}).ToList(), task.Id,
task.Id, level);
ProjectHierarchyLevel.Task);
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()
.OrderBy(x=>x.CreationDate)
.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,
};
}).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()
.OrderBy(x=>x.CreationDate)
.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,
};
}).ToList(),
project.Id,
level);
return OperationResult<ProjectSetTimeResponse>.Success(res); return OperationResult<ProjectSetTimeResponse>.Success(res);
} }

View File

@@ -1,13 +1,18 @@
using FluentValidation; using FluentValidation;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails; namespace GozareshgirProgramManager.Application.Modules.Projects.Queries.ProjectSetTimeDetails;
public class ProjectSetTimeDetailsQueryValidator:AbstractValidator<ProjectSetTimeDetailsQuery> public class ProjectSetTimeDetailsQueryValidator : AbstractValidator<ProjectSetTimeDetailsQuery>
{ {
public ProjectSetTimeDetailsQueryValidator() public ProjectSetTimeDetailsQueryValidator()
{ {
RuleFor(x => x.TaskId) RuleFor(x => x.Id)
.NotEmpty() .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,217 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
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;
}
private List<MessageDto> CreateAdditionalTimeNotes(
IEnumerable<TaskSectionAdditionalTime> additionalTimes,
Dictionary<long, string> users,
Guid taskId)
{
var notes = new List<MessageDto>();
foreach (var additionalTime in additionalTimes)
{
var addedByUserName = additionalTime.AddedByUserId.HasValue && users.TryGetValue(additionalTime.AddedByUserId.Value, out var user)
? user
: "سیستم";
var noteContent = $"⏱️ زمان اضافی: {additionalTime.Hours.TotalHours.ToString("F2")} ساعت - {(string.IsNullOrWhiteSpace(additionalTime.Reason) ? "بدون علت" : additionalTime.Reason)} - توسط {addedByUserName}";
var noteDto = new MessageDto
{
Id = Guid.NewGuid(),
TaskId = taskId,
SenderUserId = 0,
SenderName = "سیستم",
MessageType = "Note",
TextContent = noteContent,
CreationDate = additionalTime.CreationDate,
IsMine = false
};
notes.Add(noteDto);
}
return notes;
}
public async Task<OperationResult<PaginationResult<MessageDto>>> Handle(GetMessagesQuery request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId();
var skip = (request.Page - 1) * request.PageSize;
var query = _context.TaskChatMessages
.Where(m => m.TaskId == request.TaskId && !m.IsDeleted)
.Include(m => m.ReplyToMessage)
.OrderBy(m => m.CreationDate).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);
// ✅ گرفتن تمامی کاربران برای نمایش نام کامل فرستنده به جای "کاربر"
var senderUserIds = messages.Select(m => m.SenderUserId).Distinct().ToList();
var users = await _context.Users
.Where(u => senderUserIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
// ✅ گرفتن تمامی زمان‌های اضافی (Additional Times) برای نمایش به صورت نوت
var taskSections = await _context.TaskSections
.Where(ts => ts.TaskId == request.TaskId)
.Include(ts => ts.AdditionalTimes)
.ToListAsync(cancellationToken);
// ✅ تمام زمان‌های اضافی را یکجا بگیر و مرتب کن
var allAdditionalTimes = taskSections
.SelectMany(ts => ts.AdditionalTimes)
.OrderBy(at => at.CreationDate)
.ToList();
var messageDtos = new List<MessageDto>();
// ✅ ابتدا زمان‌های اضافی قبل از اولین پیام را اضافه کن (اگر پیامی وجود داشته باشد)
if (messages.Any())
{
var firstMessageDate = messages.First().CreationDate;
var additionalTimesBeforeFirstMessage = allAdditionalTimes
.Where(at => at.CreationDate < firstMessageDate)
.ToList();
messageDtos.AddRange(CreateAdditionalTimeNotes(additionalTimesBeforeFirstMessage, users, request.TaskId));
}
else
{
// ✅ اگر هیچ پیامی وجود ندارد، همه زمان‌های اضافی را نمایش بده
messageDtos.AddRange(CreateAdditionalTimeNotes(allAdditionalTimes, users, request.TaskId));
}
foreach (var message in messages)
{
// ✅ نام فرستنده را از Dictionary Users بگیر، در صورت عدم وجود "کاربر ناشناس" نمایش بده
var senderName = users.GetValueOrDefault(message.SenderUserId, "کاربر ناشناس");
var dto = new MessageDto
{
Id = message.Id,
TaskId = message.TaskId,
SenderUserId = message.SenderUserId,
SenderName = senderName,
MessageType = message.MessageType.ToString(),
TextContent = message.TextContent,
ReplyToMessageId = message.ReplyToMessageId,
IsEdited = message.IsEdited,
EditedDate = message.EditedDate,
IsPinned = message.IsPinned,
PinnedDate = message.PinnedDate,
PinnedByUserId = message.PinnedByUserId,
CreationDate = message.CreationDate,
IsMine = message.SenderUserId == currentUserId
};
if (message.ReplyToMessage != null)
{
var replySenderName = users.GetValueOrDefault(message.ReplyToMessage.SenderUserId, "کاربر ناشناس");
dto.ReplyToMessage = new MessageDto
{
Id = message.ReplyToMessage.Id,
SenderUserId = message.ReplyToMessage.SenderUserId,
SenderName = replySenderName,
TextContent = message.ReplyToMessage.TextContent,
CreationDate = message.ReplyToMessage.CreationDate
};
}
if (message.FileId.HasValue)
{
var file = await _context.UploadedFiles.FirstOrDefaultAsync(f => f.Id == message.FileId.Value, cancellationToken);
if (file != null)
{
dto.File = new MessageFileDto
{
Id = file.Id,
FileName = file.OriginalFileName,
FileUrl = file.StorageUrl ?? "",
FileSizeBytes = file.FileSizeBytes,
FileType = file.FileType.ToString(),
ThumbnailUrl = file.ThumbnailUrl,
ImageWidth = file.ImageWidth,
ImageHeight = file.ImageHeight,
DurationSeconds = file.DurationSeconds
};
}
}
messageDtos.Add(dto);
// ✅ پیدا کردن پیام بعدی (اگر وجود داشته باشد)
var currentIndex = messages.IndexOf(message);
var nextMessage = currentIndex < messages.Count - 1 ? messages[currentIndex + 1] : null;
if (nextMessage != null)
{
// ✅ زمان‌های اضافی بین این پیام و پیام بعدی
var additionalTimesBetween = allAdditionalTimes
.Where(at => at.CreationDate > message.CreationDate && at.CreationDate < nextMessage.CreationDate)
.ToList();
messageDtos.AddRange(CreateAdditionalTimeNotes(additionalTimesBetween, users, request.TaskId));
}
else
{
// ✅ این آخرین پیام است، زمان‌های اضافی بعد از آن را اضافه کن
var additionalTimesAfterLastMessage = allAdditionalTimes
.Where(at => at.CreationDate > message.CreationDate)
.ToList();
messageDtos.AddRange(CreateAdditionalTimeNotes(additionalTimesAfterLastMessage, users, request.TaskId));
}
}
// ✅ مرتب کردن نهایی تمام پیام‌ها (معمولی + نوت‌ها) بر اساس زمان ایجاد
messageDtos = messageDtos.OrderBy(m => m.CreationDate).ToList();
var response = new PaginationResult<MessageDto>()
{
List = messageDtos,
TotalCount = totalCount,
};
return OperationResult<PaginationResult<MessageDto>>.Success(response);
}
}

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,136 @@
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;
using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
using Microsoft.AspNetCore.Http;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionRevision.Commands.CreateTaskSectionRevision;
public record CreateTaskSectionRevisionCommand(string Message, List<IFormFile> Files, Guid SectionId) : IBaseCommand;
public class CreateTaskSectionRevisionCommandHandler : IBaseCommandHandler<CreateTaskSectionRevisionCommand>
{
private readonly ITaskSectionRevisionRepository _revisionRepository;
private readonly IFileStorageService _fileStorageService;
private readonly IAuthHelper _authHelper;
private readonly IUnitOfWork _unitOfWork;
private readonly IUploadedFileRepository _fileRepository;
private readonly IThumbnailGeneratorService _thumbnailService;
public CreateTaskSectionRevisionCommandHandler(ITaskSectionRevisionRepository revisionRepository,
IFileStorageService fileStorageService, IAuthHelper authHelper, IUnitOfWork unitOfWork, IUploadedFileRepository fileRepository, IThumbnailGeneratorService thumbnailService)
{
_revisionRepository = revisionRepository;
_fileStorageService = fileStorageService;
_authHelper = authHelper;
_unitOfWork = unitOfWork;
_fileRepository = fileRepository;
_thumbnailService = thumbnailService;
}
public async Task<OperationResult> Handle(CreateTaskSectionRevisionCommand request,
CancellationToken cancellationToken)
{
var currentId = _authHelper.GetCurrentUserId();
var entity = new Domain.ProjectAgg.Entities.Task.TaskSection.TaskSectionRevision(request.SectionId, request.Message, currentId!.Value);
if (request.Files is { Count: > 0 })
{
foreach (var file in request.Files)
{
if (file.Length == 0)
{
return OperationResult.ValidationError("فایل خالی است");
}
const long maxFileSize = 100 * 1024 * 1024;
if (file.Length > maxFileSize)
{
return OperationResult.ValidationError("حجم فایل بیش از حد مجاز است (حداکثر 100MB)");
}
var fileType = DetectFileType(file.ContentType, Path.GetExtension(file.FileName));
var uploadedFile = new UploadedFile(
originalFileName: file.FileName,
fileSizeBytes: file.Length,
mimeType: file.ContentType,
fileType: fileType,
category: FileCategory.TaskSectionRevision,
uploadedByUserId: currentId!.Value,
storageProvider: StorageProvider.LocalFileSystem
);
await _fileRepository.AddAsync(uploadedFile);
await _fileRepository.SaveChangesAsync();
try
{
await using var stream = file.OpenReadStream();
var uploadResult = await _fileStorageService.UploadAsync(
stream,
uploadedFile.UniqueFileName,
"TaskSectionRevision"
);
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: "TaskSectionRevision");
if (thumbnail.HasValue)
{
uploadedFile.SetThumbnail(thumbnail.Value.ThumbnailUrl);
}
}
await _fileRepository.UpdateAsync(uploadedFile);
await _fileRepository.SaveChangesAsync();
var taskRevisionFile = new TaskRevisionFile(uploadedFile.Id);
entity.AddFile(taskRevisionFile);
}
catch (Exception ex)
{
await _fileRepository.DeleteAsync(uploadedFile);
await _fileRepository.SaveChangesAsync();
return OperationResult<MessageDto>.ValidationError($"خطا در آپلود فایل: {ex.Message}");
}
}
}
await _revisionRepository.CreateAsync(entity);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
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,14 @@
using FluentValidation;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionRevision.Commands.CreateTaskSectionRevision;
public class CreateTaskSectionRevisionValidator:AbstractValidator<CreateTaskSectionRevisionCommand>
{
public CreateTaskSectionRevisionValidator()
{
RuleFor(x=>x.Message)
.NotEmpty()
.WithMessage("توضیحات اجباری است");
}
}

View File

@@ -0,0 +1,35 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionRevision.Commands.SetStatusReviewedRevision;
public record SetStatusReviewedRevisionCommand(Guid TaskSectionId):IBaseCommand;
public class SetStatusReviewedRevisionCommandHandler : IBaseCommandHandler<SetStatusReviewedRevisionCommand>
{
private readonly ITaskSectionRevisionRepository _revisionRepository;
private readonly IUnitOfWork _unitOfWork;
public SetStatusReviewedRevisionCommandHandler(ITaskSectionRevisionRepository revisionRepository, IUnitOfWork unitOfWork)
{
_revisionRepository = revisionRepository;
_unitOfWork = unitOfWork;
}
public async Task<OperationResult> Handle(SetStatusReviewedRevisionCommand request, CancellationToken cancellationToken)
{
var taskSectionRevisions = await _revisionRepository.GetByTaskSectionId(request.TaskSectionId);
if (taskSectionRevisions == null || taskSectionRevisions.Count == 0)
return OperationResult.NotFound("اصلاحی برای این بخش یافت نشد");
foreach (var revision in taskSectionRevisions)
{
revision.MarkReviewed();
}
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
}

View File

@@ -0,0 +1,92 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionRevision.Queries.TaskRevisionsByTaskSectionId;
public record TaskRevisionsByTaskSectionIdQuery(Guid TaskSectionId)
: IBaseQuery<TaskRevisionsByTaskSectionIdResponse>;
public class TaskRevisionsByTaskSectionIdQueryHandler : IBaseQueryHandler<TaskRevisionsByTaskSectionIdQuery,
TaskRevisionsByTaskSectionIdResponse>
{
private readonly IProgramManagerDbContext _dbContext;
public TaskRevisionsByTaskSectionIdQueryHandler(IProgramManagerDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<OperationResult<TaskRevisionsByTaskSectionIdResponse>> Handle(
TaskRevisionsByTaskSectionIdQuery request, CancellationToken cancellationToken)
{
var taskSectionEntity = await _dbContext.TaskSections
.Include(x=>x.Task)
.ThenInclude(x => x.Phase)
.ThenInclude(x => x.Project)
.FirstOrDefaultAsync(x => x.Id == request.TaskSectionId,
cancellationToken: cancellationToken);
if (taskSectionEntity == null)
{
return OperationResult<TaskRevisionsByTaskSectionIdResponse>.NotFound("بخش فرعی یافت نشد");
}
var taskRevisions = await _dbContext.TaskSectionRevisions
.Include(x => x.Files).Where(x => x.TaskSectionId == request.TaskSectionId)
.ToListAsync(cancellationToken);
if (taskRevisions.Count == 0)
{
return OperationResult<TaskRevisionsByTaskSectionIdResponse>.NotFound("اصلاحی یافت نشد");
}
var skill = await _dbContext.Skills.FirstOrDefaultAsync(x => x.Id == taskSectionEntity.SkillId,
cancellationToken: cancellationToken);
if (skill == null)
return OperationResult<TaskRevisionsByTaskSectionIdResponse>.NotFound("مهارت مورد نظر یافت نشد");
var user =await _dbContext.Users.FirstOrDefaultAsync(x => x.Id == taskSectionEntity.CurrentAssignedUserId,
cancellationToken: cancellationToken);
if (user == null)
return OperationResult<TaskRevisionsByTaskSectionIdResponse>.NotFound("کاربر مورد نظر یافت نشد");
var fileIds = taskRevisions.SelectMany(x => x.Files)
.Select(x => x.FileId).Distinct().ToList();
var uploadedFiles = _dbContext.UploadedFiles
.Where(x => fileIds.Contains(x.Id)).ToList();
var resItems = taskRevisions.Select(x =>
{
var itemFileIds = x.Files.Select(f => f.FileId).Distinct().ToList();
var files = uploadedFiles
.Where(f => itemFileIds.Contains(f.Id))
.Select(file => new TaskRevisionsByTaskSectionIdItemFile()
{
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
}).ToList();
return new TaskRevisionsByTaskSectionIdItem(x.Message, files,$"{x.CreationDate.ToFarsi()} {x.CreationDate:HH:mm}");
}).ToList();
var res = new TaskRevisionsByTaskSectionIdResponse(resItems, taskSectionEntity.Task.Phase.Project.Name,
taskSectionEntity.Task.Phase.Name, taskSectionEntity.Task.Name,
skill.Name,
user.FullName
);
return OperationResult<TaskRevisionsByTaskSectionIdResponse>.Success(res);
}
}

View File

@@ -0,0 +1,16 @@
using GozareshgirProgramManager.Application._Common.Models;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionRevision.Queries.TaskRevisionsByTaskSectionId;
public record TaskRevisionsByTaskSectionIdResponse(
List<TaskRevisionsByTaskSectionIdItem> Items,
string ProjectName,
string PhaseName,
string TaskName,
string SkillName,
string UserName);
public record TaskRevisionsByTaskSectionIdItem(string Message, List<TaskRevisionsByTaskSectionIdItemFile> Files,string CreationDate);
public class TaskRevisionsByTaskSectionIdItemFile:UploadedFileDto;

View File

@@ -0,0 +1,56 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionTimeRequests.Commands.AcceptTimeRequest;
public record AcceptTimeRequestCommand(Guid TimeRequestId,
Guid SectionId,TaskSectionAdditionalTimeType TimeType,int Hour,int Minute):IBaseCommand;
public class AcceptTimeRequestCommandHandler:IBaseCommandHandler<AcceptTimeRequestCommand>
{
private readonly ITaskSectionTimeRequestRepository _timeRequestRepository;
private readonly ITaskSectionRepository _taskSectionRepository;
private readonly IUnitOfWork _unitOfWork;
public AcceptTimeRequestCommandHandler(ITaskSectionTimeRequestRepository timeRequestRepository, ITaskSectionRepository taskSectionRepository, IUnitOfWork unitOfWork)
{
_timeRequestRepository = timeRequestRepository;
_taskSectionRepository = taskSectionRepository;
_unitOfWork = unitOfWork;
}
public async Task<OperationResult> Handle(AcceptTimeRequestCommand request, CancellationToken cancellationToken)
{
var timeRequest = await _timeRequestRepository.GetByIdAsync(request.TimeRequestId, cancellationToken);
if (timeRequest == null)
{
return OperationResult.NotFound("درخواست زمان شما یافت نشد");
}
var taskSection = await _taskSectionRepository.GetByIdAsync(request.SectionId, cancellationToken);
if (taskSection == null)
{
return OperationResult.NotFound("بخش فرعی وارد شده نامعتبر است");
}
if (timeRequest.RequestStatus == TaskSectionTimeRequestStatus.Accepted)
{
return OperationResult.Failure("این درخواست قبلا تایید شده است");
}
// تایید درخواست زمان
timeRequest.AcceptTimeRequest();
// اضافه کردن زمان به TaskSection
var totalMinutes = (request.Hour * 60) + request.Minute;
var additionalTime = TimeSpan.FromMinutes(totalMinutes);
taskSection.AddAdditionalTime(additionalTime, request.TimeType, timeRequest.Description);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
}

View File

@@ -0,0 +1,24 @@
using FluentValidation;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionTimeRequests.Commands.AcceptTimeRequest;
public class AcceptTimeRequestCommandValidator : AbstractValidator<AcceptTimeRequestCommand>
{
public AcceptTimeRequestCommandValidator()
{
RuleFor(c => c.TimeRequestId)
.NotEmpty().WithMessage("شناسه درخواست نمیتواند خالی باشد");
RuleFor(c => c.SectionId)
.NotEmpty().WithMessage("شناسه بخش فرعی نمیتواند خالی باشد");
RuleFor(c => c.TimeType)
.NotNull().WithMessage("نوع زمان درخواست شده نامعتبر است")
.IsInEnum();
RuleFor(c => c.Hour)
.InclusiveBetween(0, 100).WithMessage("ساعت وارد شده میتواند بین 0 تا 100 باشد");
RuleFor(c => c.Minute)
.InclusiveBetween(0, 60).WithMessage("دقیقه وارد شده میتواند بین 0 تا 60 باشد");
}
}

View File

@@ -0,0 +1,52 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionTimeRequests.Commands.CreateTimeRequest;
public record CreateTimeRequestCommand(int Hours, int Minutes, string Description,
TaskSectionTimeRequestType RequestType,Guid TaskSectionId) : IBaseCommand;
public class CreateTimeRequestCommandHandler : IBaseCommandHandler<CreateTimeRequestCommand>
{
private readonly IAuthHelper _authHelper;
private readonly ITaskSectionTimeRequestRepository _timeRequestRepository;
private readonly ITaskSectionRepository _taskSectionRepository;
private readonly IUnitOfWork _unitOfWork;
public CreateTimeRequestCommandHandler
(ITaskSectionTimeRequestRepository timeRequestRepository, IAuthHelper authHelper, IUnitOfWork unitOfWork, ITaskSectionRepository taskSectionRepository)
{
_timeRequestRepository = timeRequestRepository;
_authHelper = authHelper;
_unitOfWork = unitOfWork;
_taskSectionRepository = taskSectionRepository;
}
public async Task<OperationResult> Handle(CreateTimeRequestCommand request, CancellationToken cancellationToken)
{
var currentUser = _authHelper.GetCurrentUserId();
if (!currentUser.HasValue)
{
return OperationResult.Unauthorized();
}
if (!_taskSectionRepository.Exists(x=>x.Id == request.TaskSectionId))
{
return OperationResult.NotFound("وظیفه فرعی مورد نظر یافت نشد");
}
var requestTimeSpan = TimeSpan.FromHours(request.Hours) + TimeSpan.FromMinutes(request.Minutes);
var entity = new TaskSectionTimeRequest(currentUser.Value, request.Description, requestTimeSpan,
request.RequestType,request.TaskSectionId);
await _timeRequestRepository.CreateAsync(entity);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return OperationResult.Success();
}
}

View File

@@ -0,0 +1,20 @@
using FluentValidation;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionTimeRequests.Commands.CreateTimeRequest;
public class CreateTimeRequestValidator : AbstractValidator<CreateTimeRequestCommand>
{
public CreateTimeRequestValidator()
{
RuleFor(c => c.Hours)
.InclusiveBetween(0, 100).WithMessage("ساعت درخواست شده باید کمتر از 100 ساعت باشد");
RuleFor(c => c.Minutes)
.InclusiveBetween(0, 59)
.WithMessage("دقیقه وارد شده باید بین 0 تا 60 باشد");
RuleFor(x => x.RequestType)
.IsInEnum()
.NotNull();
}
}

View File

@@ -0,0 +1,66 @@
using GozareshgirProgramManager.Application._Common.Extensions;
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.TaskSectionTimeRequests.Queries.CreateTimeRequestDetails;
public record CreateTimeRequestDetailsResponse(List<CreateTimeRequestDetailsRevision> Revisions,
string ProjectName,string PhaseName,string SectionName,string SkillName);
public record CreateTimeRequestDetailsRevision(string Message, List<UploadedFileDto> Files,Guid Id,string CreationDate);
public record CreateTimeRequestDetailsQuery(Guid TaskSectionId) : IBaseQuery<CreateTimeRequestDetailsResponse>;
public class
CreateTimeRequestDetailsQueryHandler : IBaseQueryHandler<CreateTimeRequestDetailsQuery,
CreateTimeRequestDetailsResponse>
{
private readonly IProgramManagerDbContext _context;
public CreateTimeRequestDetailsQueryHandler(IProgramManagerDbContext context)
{
_context = context;
}
public async Task<OperationResult<CreateTimeRequestDetailsResponse>> Handle(CreateTimeRequestDetailsQuery request,
CancellationToken cancellationToken)
{
var section =await _context.TaskSections
.Include(x => x.Task)
.ThenInclude(x => x.Phase)
.ThenInclude(x => x.Project)
.Include(x => x.Skill)
.FirstOrDefaultAsync(x => x.Id == request.TaskSectionId, cancellationToken: cancellationToken);
if (section == null)
{
throw new BadRequestException("بخش فرعی نامعتبر است");
}
var revisions = await _context.TaskSectionRevisions.Where(x =>
x.TaskSectionId == request.TaskSectionId && x.Status == RevisionReviewStatus.Pending).ToListAsync(cancellationToken: cancellationToken);
var fileIds = revisions.SelectMany(x => x.Files)
.Select(x => x.FileId).ToList();
var files =await _context.UploadedFiles
.Where(x => fileIds.Contains(x.Id)).ToListAsync(cancellationToken: cancellationToken);
var resItem = revisions.Select(x =>
{
var selectFileIds = x.Files.Select(f => f.FileId).ToList();
var filesDto = files.Where(f => selectFileIds.Contains(f.Id))
.Select(f => f.ToDto()).ToList();
return new CreateTimeRequestDetailsRevision(x.Message, filesDto,x.Id,x.CreationDate.ToFarsi());
}).ToList();
var res = new CreateTimeRequestDetailsResponse(resItem,section.Task.Phase.Project.Name,section.Task.Phase.Name,section.Task.Name,
section.Skill!.Name);
return OperationResult<CreateTimeRequestDetailsResponse>.Success(res);
}
}

View File

@@ -0,0 +1,11 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList;
namespace GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList;
public interface IWorkflowProvider
{
WorkflowType Type { get; }
Task<List<WorkflowListItem>> GetItems(long currentUserId, IProgramManagerDbContext context, CancellationToken cancellationToken);
Task<int> GetCount(long currentUserId, IProgramManagerDbContext context, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,31 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList.Providers;
public class NotAssignedWorkflowProvider : IWorkflowProvider
{
public WorkflowType Type => WorkflowType.NotAssigned;
public async Task<List<WorkflowListItem>> GetItems(long currentUserId, IProgramManagerDbContext context, CancellationToken cancellationToken)
{
// Assuming 0 means unassigned in CurrentAssignedUserId
var sections = await context.TaskSections
.Where(x => x.CurrentAssignedUserId == 0)
.ToListAsync(cancellationToken);
return sections.Select(ts => new WorkflowListItem
{
EntityId = ts.Id,
Title = "تخصیص‌ نیافته",
Type = WorkflowType.NotAssigned
}).ToList();
}
public async Task<int> GetCount(long currentUserId, IProgramManagerDbContext context, CancellationToken cancellationToken)
{
return await context.TaskSections
.Where(x => x.CurrentAssignedUserId == 0)
.CountAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,41 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList.Providers;
public class RejectedRevisionsWorkflowProvider : IWorkflowProvider
{
public WorkflowType Type => WorkflowType.Rejected;
public async Task<List<WorkflowListItem>> GetItems(long currentUserId, IProgramManagerDbContext context, CancellationToken cancellationToken)
{
var query = from revision in context.TaskSectionRevisions
.Where(x => x.Status == RevisionReviewStatus.Pending)
join taskSection in context.TaskSections
on revision.TaskSectionId equals taskSection.Id
where taskSection.CurrentAssignedUserId == currentUserId
select taskSection;
var sections = await query.ToListAsync(cancellationToken);
return sections.Select(ts => new WorkflowListItem
{
EntityId = ts.Id,
Title = "برگشت از سمت مدیر",
Type = WorkflowType.Rejected
}).ToList();
}
public async Task<int> GetCount(long currentUserId, IProgramManagerDbContext context, CancellationToken cancellationToken)
{
var query = from revision in context.TaskSectionRevisions
.Where(x => x.Status == RevisionReviewStatus.Pending)
join taskSection in context.TaskSections
on revision.TaskSectionId equals taskSection.Id
where taskSection.CurrentAssignedUserId == currentUserId
select revision.Id;
return await query.CountAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,50 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList.Providers;
namespace GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList;
public record WorkflowCountResponse(int Total, int Rejected, int NotAssigned, int PendingForApproval);
public record WorkflowCountQuery() : IBaseQuery<WorkflowCountResponse>;
public class WorkflowCountQueryHandler : IBaseQueryHandler<WorkflowCountQuery, WorkflowCountResponse>
{
private readonly IProgramManagerDbContext _context;
private readonly IAuthHelper _authHelper;
private readonly IEnumerable<IWorkflowProvider> _providers;
public WorkflowCountQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper, IEnumerable<IWorkflowProvider> providers)
{
_context = context;
_authHelper = authHelper;
_providers = providers;
}
public async Task<OperationResult<WorkflowCountResponse>> Handle(WorkflowCountQuery request, CancellationToken cancellationToken)
{
long currentUserId = _authHelper.GetCurrentUserId()!.Value;
int rejectedCount = 0;
int notAssignedCount = 0;
int pendingForApprovalCount = 0;
foreach (var provider in _providers)
{
var count = await provider.GetCount(currentUserId, _context, cancellationToken);
switch (provider.Type)
{
case WorkflowType.Rejected:
rejectedCount += count; break;
case WorkflowType.NotAssigned:
notAssignedCount += count; break;
case WorkflowType.PendingForApproval:
pendingForApprovalCount += count; break;
}
}
var total = rejectedCount + notAssignedCount + pendingForApprovalCount;
var response = new WorkflowCountResponse(total, rejectedCount, notAssignedCount, pendingForApprovalCount);
return OperationResult<WorkflowCountResponse>.Success(response);
}
}

View File

@@ -0,0 +1,56 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities.Task.TaskSection;
using GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList.Providers;
namespace GozareshgirProgramManager.Application.Modules.Workflows.Queries.WorkflowList;
public record WorkflowListResponse(List<WorkflowListItem>Items);
public class WorkflowListItem
{
public string Title { get; set; }
public WorkflowType Type { get; set; }
public Guid EntityId { get; set; }
}
public enum WorkflowType
{
Rejected,
NotAssigned,
PendingForApproval,
}
public record WorkflowListQuery():IBaseQuery<WorkflowListResponse>;
public class WorkflowListQueryHandler:IBaseQueryHandler<WorkflowListQuery,WorkflowListResponse>
{
private readonly IProgramManagerDbContext _context;
private readonly IAuthHelper _authHelper;
private readonly IEnumerable<IWorkflowProvider> _providers;
public WorkflowListQueryHandler(IProgramManagerDbContext context,
IAuthHelper authHelper,
IEnumerable<IWorkflowProvider> providers)
{
_context = context;
_authHelper = authHelper;
_providers = providers;
}
public async Task<OperationResult<WorkflowListResponse>> Handle(WorkflowListQuery request, CancellationToken cancellationToken)
{
var currentUserId = _authHelper.GetCurrentUserId()!.Value;
var items = new List<WorkflowListItem>();
foreach (var provider in _providers)
{
var providerItems = await provider.GetItems(currentUserId, _context, cancellationToken);
if (providerItems?.Count > 0)
items.AddRange(providerItems);
}
var res = new WorkflowListResponse(items);
return OperationResult<WorkflowListResponse>.Success(res);
}
}

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

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