diff --git a/.gitignore b/.gitignore index 89d3b553..6ea6524b 100644 --- a/.gitignore +++ b/.gitignore @@ -368,3 +368,6 @@ MigrationBackup/ # Storage folder - ignore all uploaded files, thumbnails, and temporary files ServiceHost/Storage +.env +.env.* + diff --git a/0_Framework/Application/StaticWorkshopAccounts.cs b/0_Framework/Application/StaticWorkshopAccounts.cs index 44491a69..e1e340db 100644 --- a/0_Framework/Application/StaticWorkshopAccounts.cs +++ b/0_Framework/Application/StaticWorkshopAccounts.cs @@ -31,8 +31,9 @@ public static class StaticWorkshopAccounts /// 381 - مهدی قربانی /// 392 - عمار حسن دوست /// 20 - سمیرا الهی نیا + /// 322 - ماهان چمنی /// - public static List StaticAccountIds = [2, 3, 380, 381, 392, 20, 476]; + public static List StaticAccountIds = [2, 3, 380, 381, 392, 20, 476,322]; /// /// این تاریخ در جدول اکانت لفت ورک به این معنیست diff --git a/CompanyManagment.Application/RollCallApplication.cs b/CompanyManagment.Application/RollCallApplication.cs index e73e4c3e..e618c3b0 100644 --- a/CompanyManagment.Application/RollCallApplication.cs +++ b/CompanyManagment.Application/RollCallApplication.cs @@ -447,8 +447,7 @@ public class RollCallApplication : IRollCallApplication return operation.Failed("کارمند در بازه انتخاب شده مرخصی ساعتی دارد"); } - if (newRollCallDates == null || !newRollCallDates.All(x => employeeStatuses.Any(y => x.StartDate.Value.Date >= y.StartDateGr.Date && x.EndDate.Value.Date <= y.EndDateGr.Date))) - return operation.Failed("کارمند در بازه وارد شده غیر فعال است"); + @@ -458,7 +457,10 @@ public class RollCallApplication : IRollCallApplication _rollCallDomainService.GetEmployeeShiftDateByRollCallStartDate(command.WorkshopId, command.EmployeeId, x.StartDate!.Value,x.EndDate.Value); }); - + if (newRollCallDates == null || !newRollCallDates.All(x => employeeStatuses.Any(y => x.ShiftDate.Date >= y.StartDateGr.Date && x.ShiftDate.Date <= y.EndDateGr.Date))) + return operation.Failed("کارمند در بازه وارد شده غیر فعال است"); + + if (newRollCallDates.Any(x => x.ShiftDate.Date != date.Date)) { return operation.Failed("حضور غیاب در حال ویرایش را نمیتوانید از تاریخ شیفت عقب تر یا جلو تر ببرید"); @@ -487,8 +489,8 @@ public class RollCallApplication : IRollCallApplication - if (newRollCallDates == null || !newRollCallDates.All(x => employeeStatuses.Any(y => x.StartDate.Value.Date >= y.StartDateGr.Date - && x.EndDate.Value.Date <= y.EndDateGr.Date))) + if (newRollCallDates == null || !newRollCallDates.All(x => employeeStatuses.Any(y => x.ShiftDate.Date >= y.StartDateGr.Date + && x.ShiftDate.Date <= y.EndDateGr.Date))) return operation.Failed("کارمند در بازه وارد شده غیر فعال است"); @@ -632,9 +634,6 @@ public class RollCallApplication : IRollCallApplication return operation.Failed("کارمند در بازه انتخاب شده مرخصی ساعتی دارد"); } - if (newRollCallDates == null || !newRollCallDates.All(x => employeeStatuses.Any(y => x.StartDate.Value.Date >= y.StartDateGr.Date && x.EndDate.Value.Date <= y.EndDateGr.Date))) - return operation.Failed("کارمند در بازه وارد شده غیر فعال است"); - newRollCallDates.ForEach(x => { @@ -642,6 +641,11 @@ public class RollCallApplication : IRollCallApplication _rollCallDomainService.GetEmployeeShiftDateByRollCallStartDate(command.WorkshopId, command.EmployeeId, x.StartDate!.Value,x.EndDate.Value); }); + + if (newRollCallDates == null || !newRollCallDates.All(x => employeeStatuses.Any(y => x.ShiftDate.Date >= y.StartDateGr.Date && x.ShiftDate.Date <= y.EndDateGr.Date))) + return operation.Failed("کارمند در بازه وارد شده غیر فعال است"); + + if (newRollCallDates.Any(x => x.ShiftDate.Date != date.Date)) { return operation.Failed("حضور غیاب در حال ویرایش را نمیتوانید از تاریخ شیفت عقب تر یا جلو تر ببرید"); @@ -664,7 +668,7 @@ public class RollCallApplication : IRollCallApplication && (y.StartDate.Value.Date <= x.ContractEndGr.Date)))) return operation.Failed("برای بازه های وارد شده فیش حقوقی ثبت شده است"); - if (newRollCallDates == null || !newRollCallDates.All(x => employeeStatuses.Any(y => x.StartDate.Value.Date >= y.StartDateGr.Date && x.EndDate.Value.Date <= y.EndDateGr.Date))) + if (newRollCallDates == null || !newRollCallDates.All(x => employeeStatuses.Any(y => x.ShiftDate.Date >= y.StartDateGr.Date && x.ShiftDate.Date <= y.EndDateGr.Date))) return operation.Failed("کارمند در بازه وارد شده غیر فعال است"); var currentDayRollCall = employeeRollCalls.FirstOrDefault(x => x.EndDate == null); diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs index 245d8b39..186f47b2 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs @@ -28,26 +28,25 @@ public class SendMessageCommandHandler : IBaseCommandHandler> Handle(SendMessageCommand request, CancellationToken cancellationToken) + public async Task> Handle(SendMessageCommand request, + CancellationToken cancellationToken) { var currentUserId = _authHelper.GetCurrentUserId() ?? throw new UnAuthorizedException("کاربر احراز هویت نشده است"); @@ -57,75 +56,21 @@ public class SendMessageCommandHandler : IBaseCommandHandler.NotFound("تسک یافت نشد"); } - + Guid? uploadedFileId = null; if (request.File != null) { - if (request.File.Length == 0) - { - return OperationResult.ValidationError("فایل خالی است"); - } - - const long maxFileSize = 100 * 1024 * 1024; - if (request.File.Length > maxFileSize) - { - return OperationResult.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 + var uploadedFile = await _fileUploadService.UploadFileAsync + ( + request.File, + FileCategory.TaskChatMessage, + currentUserId ); - - await _fileRepository.AddAsync(uploadedFile); - await _fileRepository.SaveChangesAsync(); - - try + if (!uploadedFile.IsSuccess) { - 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.ValidationError($"خطا در آپلود فایل: {ex.Message}"); + return OperationResult.Failure(uploadedFile.ErrorMessage ?? "خطا در آپلود فایل"); } + uploadedFileId = uploadedFile.FileId!.Value; } var message = new TaskChatMessage( @@ -209,4 +154,4 @@ public class SendMessageCommandHandler : IBaseCommandHandler +/// سرویس آپلود و مدیریت کامل فایل +/// این سرویس تمام مراحل آپلود، ذخیره، تولید thumbnail و... را انجام می‌دهد +/// +public interface IFileUploadService +{ + /// + /// آپلود فایل با تمام مراحل پردازش + /// + /// فایل برای آپلود + /// دسته‌بندی فایل + /// شناسه کاربر آپلودکننده + /// حداکثر حجم مجاز فایل (پیش‌فرض: 100MB) + /// شناسه فایل آپلود شده یا null در صورت خطا + Task UploadFileAsync( + IFormFile file, + FileCategory category, + long uploadedByUserId, + long maxFileSizeBytes = 100 * 1024 * 1024); + + /// + /// آپلود فایل با Stream + /// + Task UploadFileFromStreamAsync( + Stream fileStream, + string fileName, + string contentType, + FileCategory category, + long uploadedByUserId, + long maxFileSizeBytes = 100 * 1024 * 1024); +} + +/// +/// نتیجه عملیات آپلود فایل +/// +public class FileUploadResult +{ + public bool IsSuccess { get; set; } + public Guid? FileId { get; set; } + public string? ErrorMessage { get; set; } + public string? StorageUrl { get; set; } + public string? ThumbnailUrl { get; set; } + + public static FileUploadResult Success(Guid fileId, string storageUrl, string? thumbnailUrl = null) + { + return new FileUploadResult + { + IsSuccess = true, + FileId = fileId, + StorageUrl = storageUrl, + ThumbnailUrl = thumbnailUrl + }; + } + + public static FileUploadResult Failed(string errorMessage) + { + return new FileUploadResult + { + IsSuccess = false, + ErrorMessage = errorMessage + }; + } +} + diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Constants/ProgramManagerPermissionCode.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Constants/ProgramManagerPermissionCode.cs index bd4230c3..7c71e909 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Constants/ProgramManagerPermissionCode.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Constants/ProgramManagerPermissionCode.cs @@ -149,6 +149,22 @@ public static class ProgramManagerPermissionCode { public const int Code = 990111; } + + /// + /// اولویت بندی + /// + public static class Priority + { + public const int Code = 990112; + } + + /// + /// ایجاد تسک باگ + /// + public static class CreateBug + { + public const int Code = 990113; + } } #endregion @@ -226,11 +242,26 @@ public static class ProgramManagerPermissionCode { public const int Code = 990208; } + + /// + /// رد با تایید اتمام اجرا + /// + public static class RejectOrApproveTaskComplete + { + public const int Code = 990209; + } + } + + #endregion + + #region Workflow[تب کارپوشه] + public static class Workflow + { + public const int Code = 9903; } #endregion - public static Dictionary GetAllCodes() { var result = new Dictionary(); diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs index 283e2b82..3b4a1ed7 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs @@ -93,6 +93,7 @@ public static class DependencyInjection // File Storage Services services.AddScoped(); services.AddScoped(); + services.AddScoped(); // JWT Settings services.Configure(configuration.GetSection("JwtSettings")); diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/FileUploadService.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/FileUploadService.cs new file mode 100644 index 00000000..ec50de7e --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/FileUploadService.cs @@ -0,0 +1,247 @@ +using GozareshgirProgramManager.Application.Services.FileManagement; +using GozareshgirProgramManager.Domain.FileManagementAgg.Entities; +using GozareshgirProgramManager.Domain.FileManagementAgg.Enums; +using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement; + +/// +/// پیاده‌سازی سرویس آپلود کامل فایل +/// +public class FileUploadService : IFileUploadService +{ + private readonly IUploadedFileRepository _fileRepository; + private readonly IFileStorageService _fileStorageService; + private readonly IThumbnailGeneratorService _thumbnailService; + private readonly ILogger _logger; + + public FileUploadService( + IUploadedFileRepository fileRepository, + IFileStorageService fileStorageService, + IThumbnailGeneratorService thumbnailService, + ILogger logger) + { + _fileRepository = fileRepository; + _fileStorageService = fileStorageService; + _thumbnailService = thumbnailService; + _logger = logger; + } + + public async Task UploadFileAsync( + IFormFile file, + FileCategory category, + long uploadedByUserId, + long maxFileSizeBytes = 100 * 1024 * 1024) + { + try + { + // اعتبارسنجی ورودی + if (file.Length == 0) + { + return FileUploadResult.Failed("فایل خالی است"); + } + + if (file.Length > maxFileSizeBytes) + { + var maxSizeMb = maxFileSizeBytes / (1024 * 1024); + return FileUploadResult.Failed($"حجم فایل بیش از حد مجاز است (حداکثر {maxSizeMb}MB)"); + } + + using var stream = file.OpenReadStream(); + return await UploadFileFromStreamAsync( + stream, + file.FileName, + file.ContentType, + category, + uploadedByUserId, + maxFileSizeBytes); + } + catch (Exception ex) + { + _logger.LogError(ex, "خطا در آپلود فایل {FileName}", file.FileName); + return FileUploadResult.Failed($"خطا در آپلود فایل: {ex.Message}"); + } + } + + public async Task UploadFileFromStreamAsync( + Stream fileStream, + string fileName, + string contentType, + FileCategory category, + long uploadedByUserId, + long maxFileSizeBytes = 100 * 1024 * 1024) + { + UploadedFile? uploadedFile = null; + + try + { + // تشخیص نوع فایل + var fileType = DetectFileType(contentType, Path.GetExtension(fileName)); + + // ایجاد رکورد فایل در دیتابیس + uploadedFile = new UploadedFile( + originalFileName: fileName, + fileSizeBytes: fileStream.Length, + mimeType: contentType, + fileType: fileType, + category: category, + uploadedByUserId: uploadedByUserId, + storageProvider: StorageProvider.LocalFileSystem + ); + + await _fileRepository.AddAsync(uploadedFile); + await _fileRepository.SaveChangesAsync(); + + // آپلود فایل به استوریج + var categoryFolder = GetCategoryFolderName(category); + var uploadResult = await _fileStorageService.UploadAsync( + fileStream, + uploadedFile.UniqueFileName, + categoryFolder + ); + + // به‌روزرسانی اطلاعات آپلود + uploadedFile.CompleteUpload(uploadResult.StoragePath, uploadResult.StorageUrl); + + // پردازش‌های خاص بر اساس نوع فایل + string? thumbnailUrl = null; + if (fileType == FileType.Image) + { + thumbnailUrl = await ProcessImageAsync(uploadedFile, uploadResult.StoragePath, categoryFolder); + } + else if (fileType == FileType.Video) + { + thumbnailUrl = await ProcessVideoAsync(uploadedFile, uploadResult.StoragePath, categoryFolder); + } + + // ذخیره تغییرات نهایی + await _fileRepository.UpdateAsync(uploadedFile); + await _fileRepository.SaveChangesAsync(); + + _logger.LogInformation( + "فایل {FileName} با شناسه {FileId} با موفقیت آپلود شد", + fileName, + uploadedFile.Id); + + return FileUploadResult.Success(uploadedFile.Id, uploadResult.StorageUrl, thumbnailUrl); + } + catch (Exception ex) + { + _logger.LogError(ex, "خطا در آپلود فایل {FileName}", fileName); + + // در صورت خطا، فایل آپلود شده را حذف کنیم + if (uploadedFile != null) + { + try + { + await _fileRepository.DeleteAsync(uploadedFile); + await _fileRepository.SaveChangesAsync(); + } + catch (Exception deleteEx) + { + _logger.LogError(deleteEx, "خطا در حذف فایل ناموفق {FileId}", uploadedFile.Id); + } + } + + return FileUploadResult.Failed($"خطا در آپلود فایل: {ex.Message}"); + } + } + + /// + /// پردازش تصویر (ابعاد و thumbnail) + /// + private async Task ProcessImageAsync(UploadedFile file, string storagePath, string categoryFolder) + { + try + { + // دریافت ابعاد تصویر + var dimensions = await _thumbnailService.GetImageDimensionsAsync(storagePath); + if (dimensions.HasValue) + { + file.SetImageDimensions(dimensions.Value.Width, dimensions.Value.Height); + } + + // تولید thumbnail + var thumbnail = await _thumbnailService.GenerateImageThumbnailAsync( + storagePath, + category: categoryFolder); + + if (thumbnail.HasValue) + { + file.SetThumbnail(thumbnail.Value.ThumbnailUrl); + return thumbnail.Value.ThumbnailUrl; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "خطا در پردازش تصویر {FileId}", file.Id); + } + + return null; + } + + /// + /// پردازش ویدیو (thumbnail) + /// + private async Task ProcessVideoAsync(UploadedFile file, string storagePath, string categoryFolder) + { + try + { + // تولید thumbnail از ویدیو + var thumbnail = await _thumbnailService.GenerateVideoThumbnailAsync( + storagePath, + category: categoryFolder); + + if (thumbnail.HasValue) + { + file.SetThumbnail(thumbnail.Value.ThumbnailUrl); + return thumbnail.Value.ThumbnailUrl; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "خطا در پردازش ویدیو {FileId}", file.Id); + } + + return null; + } + + /// + /// تشخیص نوع فایل از روی MIME type و extension + /// + 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; + } + + /// + /// دریافت نام پوشه بر اساس دسته‌بندی + /// + private string GetCategoryFolderName(FileCategory category) + { + return category switch + { + FileCategory.TaskChatMessage => "TaskChatMessage", + FileCategory.TaskAttachment => "TaskAttachment", + FileCategory.ProjectDocument => "ProjectDocument", + FileCategory.UserProfilePhoto => "UserProfilePhoto", + FileCategory.Report => "Report", + _ => "Other" + }; + } +} + diff --git a/ServiceHost/Areas/Admin/Pages/Accounts/Account/CreateRole.cshtml b/ServiceHost/Areas/Admin/Pages/Accounts/Account/CreateRole.cshtml index 5172224a..0dbf4b3c 100644 --- a/ServiceHost/Areas/Admin/Pages/Accounts/Account/CreateRole.cshtml +++ b/ServiceHost/Areas/Admin/Pages/Accounts/Account/CreateRole.cshtml @@ -1216,10 +1216,31 @@ - + + + +
+ + + + +
+ + +
+ + + + +
+ @@ -1310,8 +1331,28 @@ - + + +
+ + + +
+ + + + +
+ + + + +
diff --git a/ServiceHost/Areas/Admin/Pages/Accounts/Account/EditRole.cshtml b/ServiceHost/Areas/Admin/Pages/Accounts/Account/EditRole.cshtml index f7193980..10f6af58 100644 --- a/ServiceHost/Areas/Admin/Pages/Accounts/Account/EditRole.cshtml +++ b/ServiceHost/Areas/Admin/Pages/Accounts/Account/EditRole.cshtml @@ -1202,15 +1202,35 @@ -
- - +
+ + -
-
+ + + +
+ + + + +
+ + +
+ + + + +
+ @@ -1292,17 +1312,36 @@ -
- - +
+ + -
+
+ + +
+ + + + +
+ + + +
+ + + +
-