Compare commits

...

43 Commits

Author SHA1 Message Date
9475c786d3 add endpoint to remove employee roll calls by date 2026-02-05 11:58:52 +03:30
dddc4b143a add edit functionality for employee roll call details 2026-02-05 11:29:41 +03:30
3dace574ff refactor date parsing method in RollCallCaseHistoryController 2026-02-02 19:08:25 +03:30
bf2a102a55 add Excel export functionality for roll call case history 2026-02-02 19:07:11 +03:30
88744bd4cf add endpoint to download case history as Excel file 2026-02-02 17:54:33 +03:30
5942075dd6 refactor roll call case history controller and add total working hours endpoint 2026-02-02 17:31:37 +03:30
5c3c9739d1 fix regex and validation for rollcall history case details 2026-01-19 15:30:28 +03:30
355ec72140 comment excel data 2026-01-18 14:17:23 +03:30
90fa0ac8f8 change excel download api controller 2026-01-14 10:20:58 +03:30
eb8352e8fc add rollcall download excel controller. 2026-01-14 09:37:55 +03:30
4c7599b568 add print to rollcall case history 2026-01-13 18:50:18 +03:30
d179c90c48 change paginated to list 2026-01-13 18:31:10 +03:30
8850328fd4 add GetEditDetails controller method 2026-01-13 16:08:13 +03:30
915f16c7c0 add rollcall case history upsert 2026-01-13 14:03:10 +03:30
0d72392701 complete rollcall history details 2026-01-12 10:48:19 +03:30
8f6007835c complete rollcallCase history details by employee 2026-01-11 17:25:56 +03:30
05d860795e add rollcall title case history query 2026-01-11 15:12:08 +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
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
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
80a58f8cdc feat: add TaskId to ProjectBoardListQueryHandler and ProjectBoardListResponse for enhanced task tracking 2026-01-08 12:14:26 +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
c8dddabdff fix: correct logic for editing text messages in TaskChatMessage.cs 2026-01-08 11:05:36 +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
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
95d66c2d89 feat: enhance message queries to display real sender names and add system notes for additional times 2026-01-07 11:17:16 +03:30
609daf4353 feat: update file storage paths and enhance thumbnail generation with category support 2026-01-07 10:50:23 +03:30
a81e01ce2b Remove Storage folder from git tracking 2026-01-07 10:44:09 +03:30
2cd838a5e3 feat: enhance thumbnail generation with category support and update storage paths 2026-01-07 10:25:33 +03:30
34bd7ba444 add set file message to TaskChatMessage.cs 2026-01-06 12:21:12 +03:30
43b124664e feat: integrate authentication checks in message command handlers 2026-01-05 16:06:35 +03:30
d2dd67343b feat: add file management entities and services for chat message handling 2026-01-05 15:40:06 +03:30
3d2b5ff6bd feat: implement TaskChatMessage entity and repository for chat message management 2026-01-05 11:45:37 +03:30
121 changed files with 5612 additions and 583 deletions

4
.gitignore vendored
View File

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

View File

@@ -7,7 +7,7 @@ namespace _0_Framework.Application;
public class PagedResult<T> where T : class
{
public int TotalCount { get; set; }
public List<T> List { get; set; }
public List<T> List { get; set; } = [];
}
public class PagedResult<T,TMeta>:PagedResult<T> where T : class
{

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

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using _0_Framework.Application;
using _0_Framework.Domain;
using CompanyManagment.App.Contracts.RollCall;
using CompanyManagment.App.Contracts.WorkingHoursTemp;
@@ -91,5 +92,9 @@ namespace Company.Domain.RollCallAgg
Task<List<RollCall>> GetRollCallsUntilNowWithWorkshopIdEmployeeIds(long workshopId, List<long> employeeIds,
DateTime fromDate);
#endregion
Task<PagedResult<RollCallCaseHistoryTitleDto>> GetCaseHistoryTitles(long workshopId,RollCallCaseHistorySearchModel searchModel);
Task<List<RollCallCaseHistoryDetail>> GetCaseHistoryDetails(long workshopId,
string titleId, RollCallCaseHistorySearchModel searchModel);
}
}

View File

@@ -1,6 +1,12 @@
using _0_Framework.Excel;
using _0_Framework.Application;
using CompanyManagment.App.Contracts.RollCall;
using OfficeOpenXml;
using OfficeOpenXml.Drawing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace CompanyManagement.Infrastructure.Excel.RollCall;
@@ -308,6 +314,111 @@ public class RollCallExcelGenerator : ExcelGenerator
return package.GetAsByteArray();
}
public static byte[] CaseHistoryExcelForEmployee(List<RollCallCaseHistoryDetail> data, string titleId)
{
if (!Regex.IsMatch(titleId, @"^\d{4}_\d{2}$"))
throw new ArgumentException("Invalid titleId format.", nameof(titleId));
var splitDate = titleId.Split("_");
var year = Convert.ToInt32(splitDate.First());
var month = Convert.ToInt32(splitDate.Last());
var startDateFa = $"{year:D4}/{month:D2}/01";
var startDate = startDateFa.ToGeorgianDateTime();
var endDateFa = startDateFa.FindeEndOfMonth();
var endDate = endDateFa.ToGeorgianDateTime();
var dateRange = (int)(endDate.Date - startDate.Date).TotalDays + 1;
var dates = Enumerable.Range(0, dateRange).Select(x => startDate.AddDays(x)).ToList();
var safeData = data ?? new List<RollCallCaseHistoryDetail>();
var first = safeData.FirstOrDefault();
var totalWorkingTime = new TimeSpan(safeData.Sum(x => x.TotalWorkingTime.Ticks));
var viewModel = new CaseHistoryRollCallExcelForEmployeeViewModel
{
EmployeeId = first?.EmployeeId ?? 0,
DateGr = startDate,
PersonnelCode = first?.PersonnelCode,
EmployeeFullName = first?.EmployeeFullName,
PersianMonthName = month.ToFarsiMonthByIntNumber(),
PersianYear = year.ToString(),
TotalWorkingHoursFa = totalWorkingTime.ToFarsiHoursAndMinutes("-"),
TotalWorkingTimeSpan = $"{(int)totalWorkingTime.TotalHours}:{totalWorkingTime.Minutes:00}",
RollCalls = dates.Select((date, index) =>
{
var item = index < safeData.Count ? safeData[index] : null;
var records = item?.Records ?? new List<RollCallCaseHistoryDetailRecord>();
return new RollCallItemForEmployeeExcelViewModel
{
DateGr = date,
DateFa = date.ToFarsi(),
DayOfWeekFa = date.DayOfWeek.DayOfWeeKToPersian(),
PersonnelCode = item?.PersonnelCode,
EmployeeFullName = item?.EmployeeFullName,
IsAbsent = item?.Status == RollCallRecordStatus.Absent,
HasLeave = item?.Status == RollCallRecordStatus.Leaved,
IsHoliday = false,
TotalWorkingHours = (item?.TotalWorkingTime ?? TimeSpan.Zero).ToFarsiHoursAndMinutes("-"),
StartsItems = JoinRecords(records, r => r.StartTime),
EndsItems = JoinRecords(records, r => r.EndTime),
EnterTimeDifferences = JoinRecords(records, r => FormatSignedTimeSpan(r.EntryTimeDifference)),
ExitTimeDifferences = JoinRecords(records, r => FormatSignedTimeSpan(r.ExitTimeDifference))
};
}).ToList()
};
return CaseHistoryExcelForEmployee(viewModel);
}
public static byte[] CaseHistoryExcelForOneDay(List<RollCallCaseHistoryDetail> data, string titleId)
{
if (!Regex.IsMatch(titleId, @"^\d{4}/\d{2}/\d{2}$"))
throw new ArgumentException("Invalid titleId format.", nameof(titleId));
var dateGr = titleId.ToGeorgianDateTime();
var safeData = data ?? new List<RollCallCaseHistoryDetail>();
var viewModel = new CaseHistoryRollCallForOneDayViewModel
{
DateFa = titleId,
DateGr = dateGr,
DayOfWeekFa = dateGr.DayOfWeek.DayOfWeeKToPersian(),
RollCalls = safeData.Select(item =>
{
var records = item.Records ?? new List<RollCallCaseHistoryDetailRecord>();
return new RollCallItemForOneDayExcelViewModel
{
EmployeeFullName = item.EmployeeFullName,
PersonnelCode = item.PersonnelCode,
StartsItems = JoinRecords(records, r => r.StartTime),
EndsItems = JoinRecords(records, r => r.EndTime),
TotalWorkingHours = item.TotalWorkingTime.ToFarsiHoursAndMinutes("-")
};
}).ToList()
};
return CaseHistoryExcelForOneDay(viewModel);
}
private static string JoinRecords(IEnumerable<RollCallCaseHistoryDetailRecord> records, Func<RollCallCaseHistoryDetailRecord, string> selector)
{
var safeRecords = records ?? Enumerable.Empty<RollCallCaseHistoryDetailRecord>();
var values = safeRecords.Select(selector).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
return values.Count == 0 ? string.Empty : string.Join(Environment.NewLine, values);
}
private static string FormatSignedTimeSpan(TimeSpan value)
{
if (value == TimeSpan.Zero)
return "-";
var abs = value.Duration();
var sign = value.Ticks < 0 ? "-" : "+";
return $"{(int)abs.TotalHours}:{abs.Minutes:00}{sign}";
}
private string CalculateExitMinuteDifference(TimeSpan early, TimeSpan late)
{
if (early == TimeSpan.Zero && late == TimeSpan.Zero)

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

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

View File

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

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

@@ -4,6 +4,8 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using _0_Framework.Application;
using CompanyManagment.App.Contracts.Workshop;
using Microsoft.AspNetCore.Mvc;
namespace CompanyManagment.App.Contracts.RollCall
{
@@ -125,7 +127,62 @@ namespace CompanyManagment.App.Contracts.RollCall
/// <param name="command"></param>
/// <returns></returns>
Task<OperationResult> RecalculateValues(long workshopId, List<ReCalculateRollCallValues> command);
Task<PagedResult<RollCallCaseHistoryTitleDto>> GetCaseHistoryTitles(long workshopId,RollCallCaseHistorySearchModel searchModel);
Task<List<RollCallCaseHistoryDetail>> GetCaseHistoryDetails(long workshopId, string titleId,
RollCallCaseHistorySearchModel searchModel);
Task<RollCallCaseHistoryExcelDto> DownloadCaseHistoryExcel(long workshopId, string titleId,
RollCallCaseHistorySearchModel searchModel);
}
public class RollCallCaseHistoryExcelDto
{
public byte[] Bytes { get; set; }
public string FileName { get; set; }
public string MimeType { get; set; }
}
public class RollCallCaseHistoryDetail
{
public string EmployeeFullName { get; set; }
public string PersonnelCode { get; set; }
public TimeSpan TotalWorkingTime { get; set; }
public List<RollCallCaseHistoryDetailRecord> Records { get; set; }
public RollCallRecordStatus Status { get; set; }
public long EmployeeId { get; set; }
}
public enum RollCallRecordStatus
{
Worked = 0,
Absent = 1,
Leaved = 2
}
public class RollCallCaseHistoryDetailRecord
{
public TimeSpan EntryTimeDifference { get; set; }
public string StartTime { get; set; }
public string EndTime { get; set; }
public TimeSpan ExitTimeDifference { get; set; }
}
public class RollCallCaseHistorySearchModel:PaginationRequest
{
public string StartDate { get; set; }
public string EndDate { get; set; }
public string OneDayDate { get; set; }
public long? EmployeeId { get; set; }
}
public class RollCallCaseHistoryTitleDto
{
public string Id { get; set; }
public string Title { get; set; }
}
public class ReCalculateRollCallValues
{
public long EmployeeId { get; set; }

View File

@@ -13,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\0_Framework\0_Framework.csproj" />
<ProjectReference Include="..\Company.Domain\Company.Domain.csproj" />
<ProjectReference Include="..\CompanyManagement.Infrastructure.Excel\CompanyManagement.Infrastructure.Excel.csproj" />
<ProjectReference Include="..\CompanyManagment.App.Contracts\CompanyManagment.App.Contracts.csproj" />
<ProjectReference Include="..\CompanyManagment.EFCore\CompanyManagment.EFCore.csproj" />
</ItemGroup>

View File

@@ -19,6 +19,7 @@ using Company.Domain.PaymentTransactionAgg;
using Company.Domain.RepresentativeAgg;
using Company.Domain.RollCallServiceAgg;
using Company.Domain.WorkshopAgg;
using Company.Domain.InstitutionContractSendFlagAgg;
using CompanyManagment.App.Contracts.FinancialInvoice;
using CompanyManagment.App.Contracts.FinancialStatment;
using CompanyManagment.App.Contracts.InstitutionContract;
@@ -51,6 +52,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
private readonly IPaymentTransactionRepository _paymentTransactionRepository;
private readonly IRollCallServiceRepository _rollCallServiceRepository;
private readonly ISepehrPaymentGatewayService _sepehrPaymentGatewayService;
private readonly IInstitutionContractSendFlagRepository _institutionContractSendFlagRepository;
public InstitutionContractApplication(IInstitutionContractRepository institutionContractRepository,
@@ -62,7 +64,8 @@ public class InstitutionContractApplication : IInstitutionContractApplication
IAccountApplication accountApplication, ISmsService smsService,
IFinancialInvoiceRepository financialInvoiceRepository, IHttpClientFactory httpClientFactory,
IPaymentTransactionRepository paymentTransactionRepository, IRollCallServiceRepository rollCallServiceRepository,
ISepehrPaymentGatewayService sepehrPaymentGatewayService,ILogger<SepehrPaymentGateway> sepehrGatewayLogger)
ISepehrPaymentGatewayService sepehrPaymentGatewayService,ILogger<SepehrPaymentGateway> sepehrGatewayLogger,
IInstitutionContractSendFlagRepository institutionContractSendFlagRepository)
{
_institutionContractRepository = institutionContractRepository;
_contractingPartyRepository = contractingPartyRepository;
@@ -80,6 +83,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
_rollCallServiceRepository = rollCallServiceRepository;
_sepehrPaymentGatewayService = sepehrPaymentGatewayService;
_paymentGateway = new SepehrPaymentGateway(httpClientFactory,sepehrGatewayLogger);
_institutionContractSendFlagRepository = institutionContractSendFlagRepository;
}
public OperationResult Create(CreateInstitutionContract command)
@@ -894,6 +898,7 @@ public class InstitutionContractApplication : IInstitutionContractApplication
return opration.Succcedded();
}
public void CreateContractingPartyAccount(long contractingPartyid, long accountId)
{
_institutionContractRepository.CreateContractingPartyAccount(contractingPartyid, accountId);
@@ -1820,6 +1825,59 @@ public class InstitutionContractApplication : IInstitutionContractApplication
installments.Add(lastInstallment);
return installments;
}
}
/// <summary>
/// تعیین فلگ ارسال قرارداد
/// اگر فلگ وجود نداشتن‌د ایجاد می‌کند
/// </summary>
public async Task<OperationResult> SetContractSendFlag(SetInstitutionContractSendFlagRequest request)
{
var operationResult = new OperationResult();
try
{
// بازیابی قرارداد از SQL
var contract = _institutionContractRepository.Get(request.InstitutionContractId);
if (contract == null)
return operationResult.Failed("قرارداد مورد نظر یافت نشد");
// بررسی اینکه آیا فلگ در MongoDB وجود دارد
var existingFlag = await _institutionContractSendFlagRepository
.GetByContractId(request.InstitutionContractId);
if (existingFlag != null)
{
// اگر فلگ وجود داشتن‌د، آن را اپدیت کنیم
if (request.IsSent)
{
existingFlag.MarkAsSent();
}
else
{
existingFlag.MarkAsNotSent();
}
existingFlag.UpdateLastModified();
await _institutionContractSendFlagRepository.Update(existingFlag);
}
else
{
// اگر فلگ وجود ندارد، آن را ایجاد کنیم
var newFlag = new InstitutionContractSendFlag(
request.InstitutionContractId,
request.IsSent
);
await _institutionContractSendFlagRepository.Create(newFlag);
}
return operationResult.Succcedded();
}
catch (Exception ex)
{
return operationResult.Failed($"خطا در تعیین فلگ ارسال: {ex.Message}");
}
}
}

View File

@@ -3,9 +3,11 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using _0_Framework.Application;
using _0_Framework.Domain.CustomizeCheckoutShared.Enums;
using _0_Framework.Exceptions;
using Company.Domain.CheckoutAgg;
using Company.Domain.CustomizeCheckoutAgg;
using Company.Domain.CustomizeCheckoutTempAgg;
@@ -16,6 +18,8 @@ using Company.Domain.LeaveAgg;
using Company.Domain.RollCallAgg;
using Company.Domain.RollCallAgg.DomainService;
using Company.Domain.RollCallEmployeeAgg;
using Company.Domain.WorkshopAgg;
using CompanyManagement.Infrastructure.Excel.RollCall;
using CompanyManagment.App.Contracts.Checkout;
using CompanyManagment.App.Contracts.Employee;
using CompanyManagment.App.Contracts.RollCall;
@@ -34,8 +38,9 @@ public class RollCallApplication : IRollCallApplication
private readonly ICustomizeWorkshopSettingsRepository _customizeWorkshopSettingsRepository;
private readonly ICustomizeWorkshopEmployeeSettingsRepository _customizeWorkshopEmployeeSettingsRepository;
private readonly ICustomizeCheckoutTempRepository _customizeCheckoutTempRepository;
private readonly IWorkshopRepository _workshopRepository;
public RollCallApplication(IRollCallRepository rollCallRepository, IRollCallEmployeeRepository rollCallEmployeeRepository, IEmployeeRepository employeeRepository, ILeaveRepository leaveRepository, ICustomizeCheckoutRepository customizeCheckoutRepository, ICheckoutRepository checkoutRepository, IRollCallDomainService rollCallDomainService, ICustomizeWorkshopSettingsRepository customizeWorkshopSettingsRepository, ICustomizeWorkshopEmployeeSettingsRepository customizeWorkshopEmployeeSettingsRepository, ICustomizeCheckoutTempRepository customizeCheckoutTempRepository)
public RollCallApplication(IRollCallRepository rollCallRepository, IRollCallEmployeeRepository rollCallEmployeeRepository, IEmployeeRepository employeeRepository, ILeaveRepository leaveRepository, ICustomizeCheckoutRepository customizeCheckoutRepository, ICheckoutRepository checkoutRepository, IRollCallDomainService rollCallDomainService, ICustomizeWorkshopSettingsRepository customizeWorkshopSettingsRepository, ICustomizeWorkshopEmployeeSettingsRepository customizeWorkshopEmployeeSettingsRepository, ICustomizeCheckoutTempRepository customizeCheckoutTempRepository, IWorkshopRepository workshopRepository)
{
_rollCallRepository = rollCallRepository;
_rollCallEmployeeRepository = rollCallEmployeeRepository;
@@ -47,6 +52,7 @@ public class RollCallApplication : IRollCallApplication
_customizeWorkshopSettingsRepository = customizeWorkshopSettingsRepository;
_customizeWorkshopEmployeeSettingsRepository = customizeWorkshopEmployeeSettingsRepository;
_customizeCheckoutTempRepository = customizeCheckoutTempRepository;
_workshopRepository = workshopRepository;
}
public OperationResult Create(CreateRollCall command)
@@ -858,4 +864,58 @@ public class RollCallApplication : IRollCallApplication
}
}
public async Task<PagedResult<RollCallCaseHistoryTitleDto>> GetCaseHistoryTitles(long workshopId,
RollCallCaseHistorySearchModel searchModel)
{
return await _rollCallRepository.GetCaseHistoryTitles(workshopId,searchModel);
}
public async Task<List<RollCallCaseHistoryDetail>> GetCaseHistoryDetails(long workshopId,
string titleId, RollCallCaseHistorySearchModel searchModel)
{
return await _rollCallRepository.GetCaseHistoryDetails(workshopId, titleId, searchModel);
}
public async Task<RollCallCaseHistoryExcelDto> DownloadCaseHistoryExcel(long workshopId, string titleId,
RollCallCaseHistorySearchModel searchModel)
{
var data = await _rollCallRepository
.GetCaseHistoryDetails(workshopId, titleId, searchModel);
string nameSecondPart = "";
byte[] excelBytes;
if (Regex.IsMatch(titleId, @"^\d{4}_\d{2}$"))
{
var splitDate = titleId.Split("_");
var year = Convert.ToInt32(splitDate.First());
var month = Convert.ToInt32(splitDate.Last());
var monthName = Convert.ToInt32(month).ToFarsiMonthByIntNumber();
nameSecondPart = $"{year}/{monthName}";
excelBytes = RollCallExcelGenerator.CaseHistoryExcelForEmployee(data, titleId);
}
else if (Regex.IsMatch(titleId, @"^\d{4}/\d{2}/\d{2}$"))
{
var oneDayDate = titleId.ToGeorgianDateTime();
nameSecondPart = $" {oneDayDate.DayOfWeek.DayOfWeeKToPersian()}،{titleId}";
excelBytes = RollCallExcelGenerator.CaseHistoryExcelForOneDay(data, titleId);
}
else
{
throw new BadRequestException("شناسه سر تیتر وارد شده نامعتبر است");
}
var workshopFullName = _workshopRepository.Get(workshopId)?.WorkshopFullName ?? "بدون کارگاه";
var fileName = $"{workshopFullName} - {nameSecondPart}.xlsx";
var res = new RollCallCaseHistoryExcelDto()
{
Bytes = excelBytes,
MimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
FileName = fileName
};
return res;
}
}

View File

@@ -713,10 +713,15 @@ public class EmployeeDocumentsRepository : RepositoryBase<long, EmployeeDocument
var itemsQuery = _companyContext.EmployeeDocumentItems
.Where(x => x.DocumentStatus != DocumentStatus.Unsubmitted)
.Include(x => x.EmployeeDocuments)
.ThenInclude(x => x.Workshop).ThenInclude(x => x.WorkshopEmployers).ThenInclude(x => x.Employer)
.GroupBy(x => x.WorkshopId).Select(x => new WorkshopWithEmployeeDocumentsViewModel()
.ThenInclude(x => x.Workshop)
.ThenInclude(x => x.WorkshopEmployers)
.ThenInclude(x => x.Employer)
.GroupBy(x => x.WorkshopId)
.Select(x => new WorkshopWithEmployeeDocumentsViewModel()
{
SubmittedItemsCount = x.Count(y => y.DocumentStatus == DocumentStatus.SubmittedByAdmin || y.DocumentStatus == DocumentStatus.SubmittedByClient),
SubmittedItemsCount = x
.Count(y => y.DocumentStatus == DocumentStatus.SubmittedByAdmin
|| y.DocumentStatus == DocumentStatus.SubmittedByClient),
WorkshopId = x.Key,
WorkshopFullName = x.First().EmployeeDocuments.Workshop.WorkshopName,
EmployerName = x.First().EmployeeDocuments.Workshop.WorkshopEmployers.First().Employer.FullName

View File

@@ -11,6 +11,7 @@ using Company.Domain.InstitutionContractAgg;
using Company.Domain.InstitutionContractAmendmentTempAgg;
using Company.Domain.InstitutionContractContactInfoAgg;
using Company.Domain.InstitutionContractExtensionTempAgg;
using Company.Domain.InstitutionContractSendFlagAgg;
using Company.Domain.InstitutionPlanAgg;
using Company.Domain.SmsResultAgg;
using Company.Domain.WorkshopAgg;
@@ -42,6 +43,7 @@ using AccountManagement.Application.Contracts.Account;
using Company.Domain.InstitutionContractCreationTempAgg;
using Company.Domain.RepresentativeAgg;
using Company.Domain.TemporaryClientRegistrationAgg;
using Company.Domain.InstitutionContractSendFlagAgg;
using ContractingPartyAccount = Company.Domain.ContractingPartyAccountAgg.ContractingPartyAccount;
using FinancialStatment = Company.Domain.FinancialStatmentAgg.FinancialStatment;
using String = System.String;
@@ -57,6 +59,7 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
private readonly IMongoCollection<InstitutionContractExtensionTemp> _institutionExtensionTemp;
private readonly IMongoCollection<InstitutionContractAmendmentTemp> _institutionAmendmentTemp;
private readonly IMongoCollection<InstitutionContractCreationTemp> _institutionContractCreationTemp;
private readonly IMongoCollection<InstitutionContractSendFlag> _institutionContractSendFlag;
private readonly IPlanPercentageRepository _planPercentageRepository;
private readonly ISmsService _smsService;
private readonly ISmsResultRepository _smsResultRepository;
@@ -114,6 +117,8 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
database.GetCollection<InstitutionContractAmendmentTemp>("InstitutionContractAmendmentTemp");
_institutionContractCreationTemp =
database.GetCollection<InstitutionContractCreationTemp>("InstitutionContractCreationTemp");
_institutionContractSendFlag =
database.GetCollection<InstitutionContractSendFlag>("InstitutionContractSendFlag");
}
public EditInstitutionContract GetDetails(long id)
@@ -1353,6 +1358,12 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
.Where(x => contractIds.Contains(x.InstitutionContractId))
.ToDictionaryAsync(x => x.InstitutionContractId, x => x);
// بارگذاری وضعیت ارسال قراردادها از MongoDB - کوئری مستقیم
var filter = Builders<InstitutionContractSendFlag>.Filter
.In(x => x.InstitutionContractId, contractIds);
var sendFlagsList = await _institutionContractSendFlag.Find(filter).ToListAsync();
var sendFlags = sendFlagsList.ToDictionary(x => x.InstitutionContractId, x => x.IsSent);
var financialStatements = _context.FinancialStatments.Include(x => x.FinancialTransactionList)
.Where(x => contractingPartyIds.Contains(x.ContractingPartyId)).ToList();
var res = new PagedResult<GetInstitutionContractListItemsViewModel>()
@@ -1462,7 +1473,8 @@ public class InstitutionContractRepository : RepositoryBase<long, InstitutionCon
Workshops = workshopDetails,
IsInPersonContract = workshopGroup?.CurrentWorkshops
.Any(y => y.Services.ContractInPerson) ?? true,
IsOldContract = x.contract.SigningType == InstitutionContractSigningType.Legacy
IsOldContract = x.contract.SigningType == InstitutionContractSigningType.Legacy,
InstitutionContractIsSentFlag = sendFlags.ContainsKey(x.contract.id) ? sendFlags[x.contract.id] : false
};
}).ToList()
};

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -61,6 +61,7 @@ using Company.Domain.HolidayItemAgg;
using Company.Domain.InstitutionContractAgg;
using Company.Domain.InstitutionContractContactInfoAgg;
using Company.Domain.InstitutionContractExtensionTempAgg;
using Company.Domain.InstitutionContractSendFlagAgg;
using Company.Domain.InstitutionPlanAgg;
using Company.Domain.InsuranceAgg;
using Company.Domain.InsuranceEmployeeInfoAgg;
@@ -123,6 +124,7 @@ using Company.Domain.ZoneAgg;
using CompanyManagement.Infrastructure.Excel.SalaryAid;
using CompanyManagement.Infrastructure.Mongo.EmployeeFaceEmbeddingRepo;
using CompanyManagement.Infrastructure.Mongo.InstitutionContractInsertTempRepo;
using CompanyManagement.Infrastructure.Mongo.InstitutionContractSendFlagRepo;
using CompanyManagment.App.Contracts.AdminMonthlyOverview;
using CompanyManagment.App.Contracts.AndroidApkVersion;
using CompanyManagment.App.Contracts.AuthorizedPerson;
@@ -658,6 +660,9 @@ public class PersonalBootstrapper
services.AddTransient<ICameraBugReportApplication, CameraBugReportApplication>();
services.AddTransient<ICameraBugReportRepository, CameraBugReportRepository>(); // MongoDB Implementation
// InstitutionContractSendFlag - MongoDB
services.AddTransient<IInstitutionContractSendFlagRepository, InstitutionContractSendFlagRepository>();
services.AddDbContext<CompanyContext>(x => x.UseSqlServer(connectionString));
}
}

View File

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

View File

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

View File

@@ -41,6 +41,11 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
queryable = queryable.Where(x => x.Status == request.Status);
}
if (request.UserId is > 0)
{
queryable = queryable.Where(x => x.CurrentAssignedUserId == request.UserId);
}
var data = await queryable.ToListAsync(cancellationToken);
var activityUserIds = data.SelectMany(x => x.Activities).Select(a => a.UserId).Distinct().ToList();
@@ -117,6 +122,7 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler<ProjectBoardListQu
AssignedUser = x.CurrentAssignedUserId == x.OriginalAssignedUserId ? null
: users.GetValueOrDefault(x.CurrentAssignedUserId, "ناشناس"),
SkillName = x.Skill?.Name??"-",
TaskId = x.TaskId
};
}).ToList();

View File

@@ -14,6 +14,7 @@ public class ProjectBoardListResponse
public string OriginalUser { get; set; }
public string SkillName { get; set; }
public ProjectTaskPriority TaskPriority { get; set; }
public Guid TaskId { get; set; }
}
public class ProjectProgressDto

View File

@@ -21,7 +21,7 @@ public record ProjectDeployBoardDetailTaskItem(
TimeSpan DoneTimeSpan,
int Percentage,
List<ProjectDeployBoardDetailItemSkill> Skills)
: ProjectDeployBoardDetailPhaseItem(Name, TotalTimeSpan, DoneTimeSpan,Percentage);
: ProjectDeployBoardDetailPhaseItem(Name, TotalTimeSpan, DoneTimeSpan, Percentage);
public record ProjectDeployBoardDetailItemSkill(string OriginalUserFullName, string SkillName, int TimePercentage);
@@ -73,6 +73,7 @@ public class
var doneTime = t.Sections.Aggregate(TimeSpan.Zero,
(sum, next) => sum.Add(next.GetTotalTimeSpent()));
var skills = t.Sections
.Select(s =>
{
@@ -88,7 +89,17 @@ public class
skillName,
timePercentage);
}).ToList();
var taskPercentage = (int)Math.Round(skills.Average(x => x.TimePercentage));
int taskPercentage;
if (skills.Count == 0)
{
taskPercentage = 0;
}
else
{
taskPercentage = (int)Math.Round(skills.Average(x => x.TimePercentage));
}
return new ProjectDeployBoardDetailTaskItem(
t.Name,

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -916,6 +916,17 @@ public class institutionContractController : AdminBaseController
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"قرارداد های مالی.xlsx");
}
/// <summary>
/// تنظیم وضعیت ارسال قرارداد
/// </summary>
[HttpPost("set-is-sent")]
public async Task<ActionResult<OperationResult>> SetIsSent([FromBody] SetInstitutionContractSendFlagRequest request)
{
var result = await _institutionContractApplication.SetContractSendFlag(request);
return result;
}
}
public class InstitutionContractCreationGetRepresentativeIdResponse

View File

@@ -0,0 +1,200 @@
using _0_Framework.Application;
using CompanyManagement.Infrastructure.Excel.RollCall;
using CompanyManagment.App.Contracts.RollCall;
using CompanyManagment.App.Contracts.RollCallEmployee;
using CompanyManagment.App.Contracts.Workshop;
using Microsoft.AspNetCore.Mvc;
using ServiceHost.BaseControllers;
namespace ServiceHost.Areas.Client.Controllers.RollCall;
public class RollCallCaseHistoryController : ClientBaseController
{
private readonly IRollCallApplication _rollCallApplication;
private readonly long _workshopId;
private readonly IWorkshopApplication _workshopApplication;
private readonly IRollCallEmployeeApplication _rollCallEmployeeApplication;
public RollCallCaseHistoryController(IRollCallApplication rollCallApplication,
IAuthHelper authHelper, IWorkshopApplication workshopApplication,
IRollCallEmployeeApplication rollCallEmployeeApplication)
{
_rollCallApplication = rollCallApplication;
_workshopApplication = workshopApplication;
_rollCallEmployeeApplication = rollCallEmployeeApplication;
_workshopId = authHelper.GetWorkshopId();
}
[HttpGet]
public async Task<ActionResult<PagedResult<RollCallCaseHistoryTitleDto>>> GetTitles(
RollCallCaseHistorySearchModel searchModel)
{
return await _rollCallApplication.GetCaseHistoryTitles(_workshopId, searchModel);
}
[HttpGet("details")]
public async Task<ActionResult<List<RollCallCaseHistoryDetail>>> GetDetails(string titleId,
RollCallCaseHistorySearchModel searchModel)
{
return await _rollCallApplication.GetCaseHistoryDetails(_workshopId, titleId, searchModel);
}
/// <summary>
/// ایجاد و ویرایش
/// </summary>
/// <param name="command"></param>
/// <returns></returns>
[HttpPost]
public ActionResult<OperationResult> Upsert(CreateOrEditEmployeeRollCall command)
{
command.WorkshopId = _workshopId;
return _rollCallApplication.ManualEdit(command);
}
[HttpGet("print")]
public async Task<ActionResult<List<RollCallCaseHistoryDetail>>> GetPrintDetails(string titleId,
RollCallCaseHistorySearchModel searchModel)
{
return await _rollCallApplication.GetCaseHistoryDetails(_workshopId, titleId, searchModel);
}
[HttpGet("total-working")]
public ActionResult<OperationResult<string>> OnGetTotalWorking(
string startDate,
string startTime,
string endDate,
string endTime)
{
var op = new OperationResult<string>();
const string emptyValue = "-";
if (!TryParseDateTime(startDate, startTime, out var start) ||
!TryParseDateTime(endDate, endTime, out var end))
{
return op.Succcedded(emptyValue);
}
if (start >= end)
{
return op.Succcedded(emptyValue);
}
var duration = (end - start).ToFarsiHoursAndMinutes(emptyValue);
return op.Succcedded(duration);
}
[HttpGet("excel")]
public async Task<IActionResult> GetDownload(string titleId, RollCallCaseHistorySearchModel searchModel)
{
var res =await _rollCallApplication.DownloadCaseHistoryExcel(_workshopId, titleId, searchModel);
return File(res.Bytes,
res.MimeType,
res.FileName);
}
[HttpGet("edit")]
public ActionResult<EditRollCallDetailsResult> GetEditDetails(string date, long employeeId)
{
var result = _rollCallApplication.GetWorkshopEmployeeRollCallsForDate(_workshopId, employeeId, date);
//var dates = _rollCallApplication.GetEditableDatesForManualEdit(date.ToGeorgianDateTime());
var name = _rollCallEmployeeApplication.GetByEmployeeIdAndWorkshopId(employeeId, _workshopId);
var total = new TimeSpan(result.Sum(x =>
(x.EndDate!.Value.Ticks - x.StartDate!.Value.Ticks)));
var res = new EditRollCallDetailsResult()
{
EmployeeFullName = name.EmployeeFullName,
EmployeeId = employeeId,
DateFa = date,
//EditableDates = dates,
Records = result.Select(x=>new EmployeeRollCallRecord()
{
Date = x.DateGr,
EndDate = x.EndDateFa,
EndTime = x.EndTimeString,
RollCallId = x.Id,
StartDate = x.StartDateFa,
StartTime = x.StartTimeString
}).ToList(),
TotalRollCallsDuration = total.ToFarsiHoursAndMinutes("-")
};
return res;
}
[HttpPost("edit")]
public ActionResult<OperationResult> Edit(CreateOrEditEmployeeRollCall command)
{
command.WorkshopId = _workshopId;
var result = _rollCallApplication.ManualEdit(command);
return result;
}
[HttpDelete("delete")]
public IActionResult OnPostRemoveEmployeeRollCallsInDate(RemoveEmployeeRollCallRequest request)
{
var result = _rollCallApplication.RemoveEmployeeRollCallsInDate(_workshopId, request.EmployeeId, request.Date);
return new JsonResult(new
{
success = result.IsSuccedded,
message = result.Message,
});
}
// [HttpGet("edit")]
// public ActionResult<> GetEditDetails(string date,long employeeId)
// {
// var result = _rollCallApplication.GetWorkshopEmployeeRollCallsForDate(_workshopId, employeeId, date);
// //var dates = _rollCallApplication.GetEditableDatesForManualEdit(date.ToGeorgianDateTime());
// var name = _rollCallEmployeeApplication.GetByEmployeeIdAndWorkshopId(employeeId, _workshopId);
//
// var total = new TimeSpan(result.Sum(x =>
// (x.EndDate!.Value.Ticks - x.StartDate!.Value.Ticks)));
//
// var command = new EmployeeRollCallsViewModel()
// {
// EmployeeFullName = name.EmployeeFullName,
// EmployeeId = employeeId,
// DateFa = date,
// //EditableDates = dates,
// RollCalls = result,
// TotalRollCallsDuration = total.ToFarsiHoursAndMinutes("-")
// };
// }
private static bool TryParseDateTime(string date, string time, out DateTime result)
{
result = default;
try
{
var dateTime = date.ToGeorgianDateTime();
var timeOnly = TimeOnly.Parse(time);
result = dateTime.AddTicks(timeOnly.Ticks);
return true;
}
catch
{
return false;
}
}
}
public class EditRollCallDetailsResult
{
public string EmployeeFullName { get; set; }
public long EmployeeId { get; set; }
public string DateFa { get; set; }
public string TotalRollCallsDuration { get; set; }
public List<EmployeeRollCallRecord> Records { get; set; }
}
public class RemoveEmployeeRollCallRequest
{
public long EmployeeId { get; set; }
public string Date { get; set; }
}

View File

@@ -251,8 +251,6 @@ namespace ServiceHost.Areas.Client.Pages.Company.RollCall
var hours = (int)span.TotalHours;
var minutes = span.Minutes;
if (hours > 0 && minutes > 0)
{
return new JsonResult(new

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ServiceHost.BaseControllers;
[Authorize(Policy = "AdminArea")]

View File

@@ -42,6 +42,15 @@ public class GeneralController : GeneralBaseController
currentDate
});
}
[HttpGet("persian-day-of-week")]
public ActionResult<OperationResult<string>> OnGetDayOfWeek(string dateFa)
{
var op = new OperationResult<string>();
if (!dateFa.TryToGeorgianDateTime(out DateTime date))
return op.Failed("تاریخ وارد شده نامعتبر است");
return op.Succcedded(date.DayOfWeek.DayOfWeeKToPersian());
}
// [HttpGet("pm-permissions")]
// public IActionResult GetPMPermissions()
@@ -96,44 +105,8 @@ public class GeneralController : GeneralBaseController
var statusCode = isSuccess ? "1" : "0";
return $"{baseUrl}/callback?Status={statusCode}&transactionId={transactionId}";
}
}
public class TokenReq
{
public long Amount { get; set; }
public string CallbackUrl { get; set; }
[Display(Name = "شماره فاکتور")]
[MaxLength(100)]
[Required]
[Key]
// be ezaye har pazirande bayad yekta bashad
public string invoiceID { get; set; }
[Required] public long terminalID { get; set; }
/*
* JSON Bashad
* etelaate takmili site harchi
* nabayad char khas dashte bashe (*'"xp_%!+- ...)
*/
public string Payload { get; set; } = "";
public string email { get; set; }
}
public class TokenResp
{
// if 0 = success
public int Status { get; set; }
public string AccessToken { get; set; }
}
public class PayRequest
{
[Required] [MaxLength(3000)] public string token { get; set; }
[Required] public long terminalID { get; set; }
public string nationalCode { get; set; }
}
public class SepehrGatewayPayResponse
@@ -170,9 +143,3 @@ public class SepehrGatewayPayResponse
[Display(Name = " شماره کارت ")] public string cardnumber { get; set; }
}
public class AdviceReq
{
[Display(Name = " رسید دیجیتال ")] public string digitalreceipt { get; set; }
public long Tid { get; set; }
}

View File

@@ -63,10 +63,17 @@ if (!Directory.Exists(logDirectory))
Directory.CreateDirectory(logDirectory);
}
// فقط برای فایل از Serilog استفاده می‌شود
// تنظیمات MinimumLevel از appsettings.json خوانده می‌شود
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
//NO EF Core log
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
//NO DbCommand log
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Warning)
//NO Microsoft Public log
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
//.MinimumLevel.Information()
.WriteTo.File(
path: Path.Combine(logDirectory, "gozareshgir_log.txt"),
rollingInterval: RollingInterval.Day,
@@ -373,9 +380,10 @@ builder.Services.AddParbad().ConfigureGateways(gateways =>
});
// فقط Serilog برای File استفاده می‌شه، کنسول از لاگر پیش‌فرض ASP.NET استفاده می‌کنه
builder.Host.UseSerilog((context, services, configuration) =>
if (builder.Environment.IsDevelopment())
{
builder.Host.UseSerilog((context, services, configuration) =>
{
var logConfig = configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
@@ -389,7 +397,13 @@ builder.Host.UseSerilog((context, services, configuration) =>
shared: true,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}"
);
}, writeToProviders: true); // این باعث میشه کنسول پیش‌فرض هم کار کنه
}, writeToProviders: true); // این باعث میشه کنسول پیش‌فرض هم کار کنه
}
else
{
builder.Host.UseSerilog();
}
Log.Information("SERILOG STARTED SUCCESSFULLY");
@@ -486,6 +500,24 @@ app.UseHttpsRedirection();
app.UseStaticFiles();
// Static files برای فایل‌های آپلود شده
var uploadsPath = builder.Configuration["FileStorage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "Storage");
if (!Directory.Exists(uploadsPath))
{
Directory.CreateDirectory(uploadsPath);
}
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(uploadsPath),
RequestPath = "/storage",
OnPrepareResponse = ctx =>
{
// Cache برای فایل‌ها (30 روز)
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000");
}
});
app.UseCookiePolicy();

View File

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

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