diff --git a/DadmehrGostar.sln b/DadmehrGostar.sln index 04c70593..c39485ab 100644 --- a/DadmehrGostar.sln +++ b/DadmehrGostar.sln @@ -90,7 +90,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackgroundInstitutionContra EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProgramManager", "ProgramManager", "{67AFF7B6-4C4F-464C-A90D-9BDB644D83A9}" ProjectSection(SolutionItems) = preProject - ProgramManager\TASKCHAT_ARCHITECTURE.md = ProgramManager\TASKCHAT_ARCHITECTURE.md + ProgramManager\appsettings.FileStorage.json = ProgramManager\appsettings.FileStorage.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{48F6F6A5-7340-42F8-9216-BEB7A4B7D5A1}" diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/GozareshgirProgramManager.Application.csproj b/ProgramManager/src/Application/GozareshgirProgramManager.Application/GozareshgirProgramManager.Application.csproj index 41ec592d..d28d7b48 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/GozareshgirProgramManager.Application.csproj +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/GozareshgirProgramManager.Application.csproj @@ -9,6 +9,7 @@ + @@ -18,4 +19,10 @@ + + + C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.AspNetCore.Http.Features.dll + + + diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/DeleteMessage/DeleteMessageCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/DeleteMessage/DeleteMessageCommand.cs new file mode 100644 index 00000000..4837a3f4 --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/DeleteMessage/DeleteMessageCommand.cs @@ -0,0 +1,44 @@ +using GozareshgirProgramManager.Application._Common.Models; +using GozareshgirProgramManager.Application._Common.Interfaces; +using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories; + +namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.DeleteMessage; + +public record DeleteMessageCommand(Guid MessageId) : IBaseCommand; + +public class DeleteMessageCommandHandler : IBaseCommandHandler +{ + private readonly ITaskChatMessageRepository _repository; + + public DeleteMessageCommandHandler(ITaskChatMessageRepository repository) + { + _repository = repository; + } + + public async Task Handle(DeleteMessageCommand request, CancellationToken cancellationToken) + { + // TODO: Get current user + var currentUserId = 1L; + + 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); + } + } +} diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/EditMessage/EditMessageCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/EditMessage/EditMessageCommand.cs new file mode 100644 index 00000000..c56be3bb --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/EditMessage/EditMessageCommand.cs @@ -0,0 +1,48 @@ +using GozareshgirProgramManager.Application._Common.Interfaces; +using GozareshgirProgramManager.Application._Common.Models; +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 +{ + private readonly ITaskChatMessageRepository _repository; + + public EditMessageCommandHandler(ITaskChatMessageRepository repository) + { + _repository = repository; + } + + public async Task Handle(EditMessageCommand request, CancellationToken cancellationToken) + { + // TODO: Get current user + var currentUserId = 1L; + + 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); + } + } +} diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/PinMessage/PinMessageCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/PinMessage/PinMessageCommand.cs new file mode 100644 index 00000000..e0a49a7c --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/PinMessage/PinMessageCommand.cs @@ -0,0 +1,43 @@ +using GozareshgirProgramManager.Application._Common.Interfaces; +using GozareshgirProgramManager.Application._Common.Models; +using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories; +using MediatR; + +namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.PinMessage; + +public record PinMessageCommand(Guid MessageId) : IBaseCommand; + +public class PinMessageCommandHandler : IBaseCommandHandler +{ + private readonly ITaskChatMessageRepository _repository; + + public PinMessageCommandHandler(ITaskChatMessageRepository repository) + { + _repository = repository; + } + + public async Task Handle(PinMessageCommand request, CancellationToken cancellationToken) + { + // TODO: Get current user + var currentUserId = 1L; + + 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); + } + } +} 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 new file mode 100644 index 00000000..b8b2a32c --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs @@ -0,0 +1,211 @@ +using GozareshgirProgramManager.Application._Common.Interfaces; +using GozareshgirProgramManager.Application._Common.Models; +using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs; +using GozareshgirProgramManager.Application.Services.FileManagement; +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; + +public class SendMessageCommandHandler : IBaseCommandHandler +{ + private readonly ITaskChatMessageRepository _messageRepository; + private readonly IUploadedFileRepository _fileRepository; + private readonly IProjectTaskRepository _taskRepository; + private readonly IFileStorageService _fileStorageService; + private readonly IThumbnailGeneratorService _thumbnailService; + + public SendMessageCommandHandler( + ITaskChatMessageRepository messageRepository, + IUploadedFileRepository fileRepository, + IProjectTaskRepository taskRepository, + IFileStorageService fileStorageService, + IThumbnailGeneratorService thumbnailService) + { + _messageRepository = messageRepository; + _fileRepository = fileRepository; + _taskRepository = taskRepository; + _fileStorageService = fileStorageService; + _thumbnailService = thumbnailService; + } + + public async Task> Handle(SendMessageCommand request, CancellationToken cancellationToken) + { + var currentUserId = 1L; + + var task = await _taskRepository.GetByIdAsync(request.TaskId, cancellationToken); + if (task == null) + { + return OperationResult.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 + ); + + 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); + 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}"); + } + } + + var message = new TaskChatMessage( + taskId: request.TaskId, + senderUserId: currentUserId, + messageType: request.MessageType, + textContent: request.TextContent + ); + + if (uploadedFileId.HasValue) + { + message.SetFile(uploadedFileId.Value); + } + + 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.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; + } +} diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/UnpinMessage/UnpinMessageCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/UnpinMessage/UnpinMessageCommand.cs new file mode 100644 index 00000000..7d11beb4 --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/UnpinMessage/UnpinMessageCommand.cs @@ -0,0 +1,43 @@ +using GozareshgirProgramManager.Application._Common.Interfaces; +using GozareshgirProgramManager.Application._Common.Models; +using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories; +using MediatR; + +namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.UnpinMessage; + +public record UnpinMessageCommand(Guid MessageId) : IBaseCommand; + +public class UnpinMessageCommandHandler : IBaseCommandHandler +{ + private readonly ITaskChatMessageRepository _repository; + + public UnpinMessageCommandHandler(ITaskChatMessageRepository repository) + { + _repository = repository; + } + + public async Task Handle(UnpinMessageCommand request, CancellationToken cancellationToken) + { + // TODO: Get current user + var currentUserId = 1L; + + 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); + } + } +} diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/DTOs/MessageDto.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/DTOs/MessageDto.cs new file mode 100644 index 00000000..67598d8c --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/DTOs/MessageDto.cs @@ -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"; + } + } +} + diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetMessages/GetMessagesQuery.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetMessages/GetMessagesQuery.cs new file mode 100644 index 00000000..430b93cc --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetMessages/GetMessagesQuery.cs @@ -0,0 +1,111 @@ +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.GetMessages; + +public record GetMessagesQuery( + Guid TaskId, + int Page = 1, + int PageSize = 50 +) : IBaseQuery>; + + + +public class GetMessagesQueryHandler : IBaseQueryHandler> +{ + private readonly IProgramManagerDbContext _context; + private readonly IAuthHelper _authHelper; + + public GetMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper) + { + _context = context; + _authHelper = authHelper; + } + + public async Task>> 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 messageDtos = new List(); + + foreach (var message in messages) + { + var dto = new MessageDto + { + Id = message.Id, + TaskId = message.TaskId, + SenderUserId = message.SenderUserId, + SenderName = "کاربر", // TODO: از User service + 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) + { + dto.ReplyToMessage = new MessageDto + { + Id = message.ReplyToMessage.Id, + SenderUserId = message.ReplyToMessage.SenderUserId, + SenderName = "کاربر", + 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 response = new PaginationResult() + { + List = messageDtos, + TotalCount = totalCount, + }; + + return OperationResult>.Success(response); + } +} diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetPinnedMessages/GetPinnedMessagesQuery.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetPinnedMessages/GetPinnedMessagesQuery.cs new file mode 100644 index 00000000..6a7b46ee --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetPinnedMessages/GetPinnedMessagesQuery.cs @@ -0,0 +1,73 @@ +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>; + +public class GetPinnedMessagesQueryHandler : IBaseQueryHandler> +{ + private readonly IProgramManagerDbContext _context; + private readonly IAuthHelper _authHelper; + + public GetPinnedMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper) + { + _context = context; + _authHelper = authHelper; + } + + public async Task>> 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 messageDtos = new List(); + + foreach (var message in messages) + { + var dto = new MessageDto + { + Id = message.Id, + TaskId = message.TaskId, + SenderUserId = message.SenderUserId, + 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>.Success(messageDtos); + } +} diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/SearchMessages/SearchMessagesQuery.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/SearchMessages/SearchMessagesQuery.cs new file mode 100644 index 00000000..b2f23a5c --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/SearchMessages/SearchMessagesQuery.cs @@ -0,0 +1,63 @@ +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>; + +public class SearchMessagesQueryHandler : IBaseQueryHandler> +{ + private readonly IProgramManagerDbContext _context; + private readonly IAuthHelper _authHelper; + + public SearchMessagesQueryHandler(IProgramManagerDbContext context, IAuthHelper authHelper) + { + _context = context; + _authHelper = authHelper; + } + + public async Task>> 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 messageDtos = new List(); + foreach (var message in messages) + { + var dto = new MessageDto + { + Id = message.Id, + TaskId = message.TaskId, + SenderUserId = message.SenderUserId, + SenderName = "کاربر", + MessageType = message.MessageType.ToString(), + TextContent = message.TextContent, + CreationDate = message.CreationDate, + IsMine = message.SenderUserId == currentUserId + }; + + messageDtos.Add(dto); + } + + return OperationResult>.Success(messageDtos); + } +} diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IFileStorageService.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IFileStorageService.cs new file mode 100644 index 00000000..3a429f63 --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IFileStorageService.cs @@ -0,0 +1,38 @@ +using GozareshgirProgramManager.Domain.FileManagementAgg.Entities; + +namespace GozareshgirProgramManager.Application.Services.FileManagement; + +/// +/// سرویس ذخیره‌سازی فایل +/// +public interface IFileStorageService +{ + /// + /// آپلود فایل + /// + Task<(string StoragePath, string StorageUrl)> UploadAsync( + Stream fileStream, + string uniqueFileName, + string category); + + /// + /// حذف فایل + /// + Task DeleteAsync(string storagePath); + + /// + /// دریافت فایل + /// + Task GetFileStreamAsync(string storagePath); + + /// + /// بررسی وجود فایل + /// + Task ExistsAsync(string storagePath); + + /// + /// دریافت URL فایل + /// + string GetFileUrl(string storagePath); +} + diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IThumbnailGeneratorService.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IThumbnailGeneratorService.cs new file mode 100644 index 00000000..142a4542 --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IThumbnailGeneratorService.cs @@ -0,0 +1,32 @@ +namespace GozareshgirProgramManager.Application.Services.FileManagement; + +/// +/// سرویس تولید thumbnail برای تصاویر و ویدیوها +/// +public interface IThumbnailGeneratorService +{ + /// + /// تولید thumbnail برای تصویر + /// + Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync( + string imagePath, + int width = 200, + int height = 200); + + /// + /// تولید thumbnail برای ویدیو + /// + Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync( + string videoPath); + + /// + /// حذف thumbnail + /// + Task DeleteThumbnailAsync(string thumbnailPath); + + /// + /// دریافت ابعاد تصویر + /// + Task<(int Width, int Height)?> GetImageDimensionsAsync(string imagePath); +} + diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Interfaces/IProgramManagerDbContext.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Interfaces/IProgramManagerDbContext.cs index 1a477bec..a8f8400b 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Interfaces/IProgramManagerDbContext.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Interfaces/IProgramManagerDbContext.cs @@ -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 ProjectTasks { get; set; } + DbSet TaskChatMessages { get; set; } + DbSet UploadedFiles { get; set; } + DbSet Skills { get; set; } Task SaveChangesAsync(CancellationToken cancellationToken = default); } diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Entities/UploadedFile.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Entities/UploadedFile.cs new file mode 100644 index 00000000..b256fd93 --- /dev/null +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Entities/UploadedFile.cs @@ -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; + +/// +/// فایل آپلود شده - Aggregate Root +/// مدیریت مرکزی تمام فایل‌های سیستم +/// +public class UploadedFile : EntityBase +{ + 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"; + } +} + diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileCategory.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileCategory.cs new file mode 100644 index 00000000..25e7c3f6 --- /dev/null +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileCategory.cs @@ -0,0 +1,15 @@ +namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums; + +/// +/// دسته‌بندی فایل - مشخص می‌کند فایل در کجا استفاده شده +/// +public enum FileCategory +{ + TaskChatMessage = 1, // پیام چت تسک + TaskAttachment = 2, // ضمیمه تسک + ProjectDocument = 3, // مستندات پروژه + UserProfilePhoto = 4, // عکس پروفایل کاربر + Report = 5, // گزارش + Other = 6 // سایر +} + diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileStatus.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileStatus.cs new file mode 100644 index 00000000..5781e1f7 --- /dev/null +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileStatus.cs @@ -0,0 +1,13 @@ +namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums; + +/// +/// وضعیت فایل +/// +public enum FileStatus +{ + Uploading = 1, // در حال آپلود + Active = 2, // فعال و قابل استفاده + Deleted = 5, // حذف شده (Soft Delete) + Archived = 6 // آرشیو شده +} + diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileType.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileType.cs new file mode 100644 index 00000000..63d07122 --- /dev/null +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/FileType.cs @@ -0,0 +1,15 @@ +namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums; + +/// +/// نوع فایل +/// +public enum FileType +{ + Document = 1, // اسناد (PDF, Word, Excel, etc.) + Image = 2, // تصویر + Video = 3, // ویدیو + Audio = 4, // صوت + Archive = 5, // فایل فشرده (ZIP, RAR) + Other = 6 // سایر +} + diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/StorageProvider.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/StorageProvider.cs new file mode 100644 index 00000000..a8813a56 --- /dev/null +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Enums/StorageProvider.cs @@ -0,0 +1,10 @@ +namespace GozareshgirProgramManager.Domain.FileManagementAgg.Enums; + +/// +/// نوع ذخیره‌ساز فایل +/// +public enum StorageProvider +{ + LocalFileSystem = 1, // دیسک محلی سرور +} + diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Events/FileEvents.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Events/FileEvents.cs new file mode 100644 index 00000000..df94f2df --- /dev/null +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Events/FileEvents.cs @@ -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; +} + diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Repositories/IUploadedFileRepository.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Repositories/IUploadedFileRepository.cs new file mode 100644 index 00000000..b56afa70 --- /dev/null +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/FileManagementAgg/Repositories/IUploadedFileRepository.cs @@ -0,0 +1,91 @@ +using GozareshgirProgramManager.Domain.FileManagementAgg.Entities; +using GozareshgirProgramManager.Domain.FileManagementAgg.Enums; + +namespace GozareshgirProgramManager.Domain.FileManagementAgg.Repositories; + +/// +/// Repository برای مدیریت فایل‌های آپلود شده +/// +public interface IUploadedFileRepository +{ + /// + /// دریافت فایل بر اساس شناسه + /// + Task GetByIdAsync(Guid fileId); + + /// + /// دریافت فایل بر اساس نام یکتا + /// + Task GetByUniqueFileNameAsync(string uniqueFileName); + + /// + /// دریافت لیست فایل‌های یک کاربر + /// + Task> GetUserFilesAsync(long userId, int pageNumber, int pageSize); + + /// + /// دریافت فایل‌های یک دسته خاص + /// + Task> GetByCategoryAsync(FileCategory category, int pageNumber, int pageSize); + + /// + /// دریافت فایل‌های با وضعیت خاص + /// + Task> GetByStatusAsync(FileStatus status, int pageNumber, int pageSize); + + /// + /// دریافت فایل‌های یک Reference خاص + /// + Task> GetByReferenceAsync(string entityType, string entityId); + + /// + /// جستجو در فایل‌ها بر اساس نام + /// + Task> SearchByNameAsync(string searchTerm, int pageNumber, int pageSize); + + /// + /// دریافت تعداد کل فایل‌های یک کاربر + /// + Task GetUserFilesCountAsync(long userId); + + /// + /// دریافت مجموع حجم فایل‌های یک کاربر (به بایت) + /// + Task GetUserTotalFileSizeAsync(long userId); + + /// + /// دریافت فایل‌های منقضی شده برای پاک‌سازی + /// + Task> GetExpiredFilesAsync(DateTime olderThan); + + /// + /// اضافه کردن فایل جدید + /// + Task AddAsync(UploadedFile file); + + /// + /// به‌روزرسانی فایل + /// + Task UpdateAsync(UploadedFile file); + + /// + /// حذف فیزیکی فایل (فقط برای cleanup) + /// + Task DeleteAsync(UploadedFile file); + + /// + /// ذخیره تغییرات + /// + Task SaveChangesAsync(); + + /// + /// بررسی وجود فایل + /// + Task ExistsAsync(Guid fileId); + + /// + /// بررسی وجود فایل با نام یکتا + /// + Task ExistsByUniqueFileNameAsync(string uniqueFileName); +} + diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs index b132ac62..841853cb 100644 --- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs @@ -40,14 +40,8 @@ public class TaskChatMessage : EntityBase // محتوای متنی (برای پیام‌های Text و Caption برای فایل/تصویر) public string? TextContent { get; private set; } - // اطلاعات فایل (برای پیام‌های File, Voice, Image, Video) - public string? FileUrl { get; private set; } - public string? FileName { get; private set; } - public long? FileSizeBytes { get; private set; } - public string? FileMimeType { get; private set; } - - // اطلاعات صوت (برای Voice) - public int? VoiceDurationSeconds { get; private set; } + // ارجاع به فایل (برای پیام‌های File, Voice, Image, Video) + public Guid? FileId { get; private set; } // پیام Reply public Guid? ReplyToMessageId { get; private set; } @@ -71,44 +65,21 @@ public class TaskChatMessage : EntityBase if ((MessageType == MessageType.File || MessageType == MessageType.Voice || MessageType == MessageType.Image || MessageType == MessageType.Video) - && string.IsNullOrWhiteSpace(FileUrl)) + && FileId == null) { - throw new BadRequestException("آدرس فایل نمی‌تواند خالی باشد"); - } - - if (MessageType == MessageType.Voice && VoiceDurationSeconds == null) - { - throw new BadRequestException("مدت زمان صدا باید مشخص شود"); + throw new BadRequestException("پیام‌های فایلی باید شناسه فایل داشته باشند"); } } - public void SetFileInfo(string fileUrl, string fileName, long fileSizeBytes, string mimeType) + public void SetFile(Guid fileId) { if (MessageType != MessageType.File && MessageType != MessageType.Image && MessageType != MessageType.Video && MessageType != MessageType.Voice) { - throw new BadRequestException("فقط می‌توان برای پیام‌های فایل، تصویر، ویدیو و صدا اطلاعات فایل تنظیم کرد"); + throw new BadRequestException("فقط می‌توان برای پیام‌های فایل، تصویر، ویدیو و صدا شناسه فایل تنظیم کرد"); } - FileUrl = fileUrl; - FileName = fileName; - FileSizeBytes = fileSizeBytes; - FileMimeType = mimeType; - } - - public void SetVoiceDuration(int durationSeconds) - { - if (MessageType != MessageType.Voice) - { - throw new BadRequestException("فقط می‌توان برای پیام‌های صوتی مدت زمان تنظیم کرد"); - } - - if (durationSeconds <= 0) - { - throw new BadRequestException("مدت زمان صدا باید بیشتر از صفر باشد"); - } - - VoiceDurationSeconds = durationSeconds; + FileId = fileId; } public void EditMessage(string newTextContent, long editorUserId) diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs index 8ac9b3b8..141ebaf1 100644 --- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs @@ -43,7 +43,7 @@ public interface ITaskChatMessageRepository Task> GetUserMessagesAsync(Guid taskId, long userId, int pageNumber, int pageSize); /// - /// دریافت پیام‌های با فایل (تصویر، ویدیو، فایل و...) + /// دریافت پیام‌های با فایل (تصویر، ویدیو، فایل و...) - پیام‌هایی که FileId دارند /// Task> GetMediaMessagesAsync(Guid taskId, int pageNumber, int pageSize); diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs index 1543fb97..20618330 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/DependencyInjection.cs @@ -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(); + // File Management & Task Chat + services.AddScoped(); + services.AddScoped(); + + // File Storage Services + services.AddScoped(); + services.AddScoped(); + // JWT Settings services.Configure(configuration.GetSection("JwtSettings")); diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/GozareshgirProgramManager.Infrastructure.csproj b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/GozareshgirProgramManager.Infrastructure.csproj index 63dd5c8a..0f441dc9 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/GozareshgirProgramManager.Infrastructure.csproj +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/GozareshgirProgramManager.Infrastructure.csproj @@ -16,10 +16,19 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.AspNetCore.Hosting.Abstractions.dll + + + C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\10.0.1\Microsoft.Extensions.Hosting.Abstractions.dll + + diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/20260105112925_add task chat - uploaded file.Designer.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/20260105112925_add task chat - uploaded file.Designer.cs new file mode 100644 index 00000000..aaefdec1 --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/20260105112925_add task chat - uploaded file.Designer.cs @@ -0,0 +1,1075 @@ +// +using System; +using GozareshgirProgramManager.Infrastructure.Persistence.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GozareshgirProgramManager.Infrastructure.Migrations +{ + [DbContext(typeof(ProgramManagerDbContext))] + [Migration("20260105112925_add task chat - uploaded file")] + partial class addtaskchatuploadedfile + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.CheckoutAgg.Entities.Checkout", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CheckoutEndDate") + .HasColumnType("datetime2"); + + b.Property("CheckoutStartDate") + .HasColumnType("datetime2"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DeductionFromSalary") + .HasColumnType("float"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MandatoryHours") + .HasColumnType("int"); + + b.Property("Month") + .HasColumnType("int"); + + b.Property("MonthlySalaryDefined") + .HasColumnType("float"); + + b.Property("MonthlySalaryPay") + .HasColumnType("float"); + + b.Property("RemainingHours") + .HasColumnType("int"); + + b.Property("TotalDaysWorked") + .HasColumnType("int"); + + b.Property("TotalHoursWorked") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Checkouts", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.CustomerAgg.Customer", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.FileManagementAgg.Entities.UploadedFile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DeletedByUserId") + .HasColumnType("bigint"); + + b.Property("DeletedDate") + .HasColumnType("datetime2"); + + b.Property("DurationSeconds") + .HasColumnType("int"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ImageHeight") + .HasColumnType("int"); + + b.Property("ImageWidth") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsVirusScanPassed") + .HasColumnType("bit"); + + b.Property("MimeType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ReferenceEntityId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ReferenceEntityType") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("StoragePath") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("StorageProvider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("StorageUrl") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ThumbnailUrl") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("UniqueFileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UploadDate") + .HasColumnType("datetime2"); + + b.Property("UploadedByUserId") + .HasColumnType("bigint"); + + b.Property("VirusScanDate") + .HasColumnType("datetime2"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("PhaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("SkillId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PhaseId"); + + b.HasIndex("SkillId"); + + b.ToTable("PhaseSections"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("HasAssignmentOverride") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PlannedEndDate") + .HasColumnType("datetime2"); + + b.Property("PlannedStartDate") + .HasColumnType("datetime2"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectPhase", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DeployStatus") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("HasAssignmentOverride") + .HasColumnType("bit"); + + b.Property("IsArchived") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OrderIndex") + .HasColumnType("int"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProjectPhases", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectSection", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SkillId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SkillId"); + + b.ToTable("ProjectSections"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectTask", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AllocatedTime") + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("HasAssignmentOverride") + .HasColumnType("bit"); + + b.Property("HasTimeOverride") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OrderIndex") + .HasColumnType("int"); + + b.Property("PhaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("PhaseId"); + + b.ToTable("ProjectTasks", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSection", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("CurrentAssignedUserId") + .HasColumnType("bigint"); + + b.Property("InitialDescription") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("InitialEstimatedHours") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("OriginalAssignedUserId") + .HasColumnType("bigint"); + + b.Property("SkillId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("TaskId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("SkillId"); + + b.HasIndex("TaskId"); + + b.ToTable("TaskSections", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSectionActivity", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EndNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("SectionId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("TaskSectionActivities", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSectionAdditionalTime", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AddedAt") + .HasColumnType("datetime2"); + + b.Property("AddedByUserId") + .HasColumnType("bigint"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Hours") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("Reason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("TaskSectionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("TaskSectionId"); + + b.ToTable("TaskSectionAdditionalTimes", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.RoleAgg.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("GozareshgirRoleId") + .HasColumnType("bigint"); + + b.Property("RoleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("PmRoles", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities.SalaryPaymentSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("EndSettingDate") + .HasColumnType("datetime2"); + + b.Property("HolidayWorking") + .HasColumnType("bit"); + + b.Property("MonthlySalary") + .HasColumnType("float"); + + b.Property("StartSettingDate") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("SalaryPaymentSetting", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.SkillAgg.Entities.Skill", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Skills", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DeletedDate") + .HasColumnType("datetime2"); + + b.Property("EditedDate") + .HasColumnType("datetime2"); + + b.Property("FileId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsEdited") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsPinned") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("PinnedByUserId") + .HasColumnType("bigint"); + + b.Property("PinnedDate") + .HasColumnType("datetime2"); + + b.Property("ReplyToMessageId") + .HasColumnType("uniqueidentifier"); + + b.Property("SenderUserId") + .HasColumnType("bigint"); + + b.Property("TaskId") + .HasColumnType("uniqueidentifier"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ProfilePhotoPath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("VerifyCode") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.HasKey("Id"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.UserRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("UserRefreshTokens", (string)null); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.PhaseSection", b => + { + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectPhase", "Phase") + .WithMany("PhaseSections") + .HasForeignKey("PhaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GozareshgirProgramManager.Domain.SkillAgg.Entities.Skill", "Skill") + .WithMany() + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Phase"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectPhase", b => + { + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project", "Project") + .WithMany("Phases") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectSection", b => + { + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project", "Project") + .WithMany("ProjectSections") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GozareshgirProgramManager.Domain.SkillAgg.Entities.Skill", "Skill") + .WithMany() + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Project"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectTask", b => + { + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectPhase", "Phase") + .WithMany("Tasks") + .HasForeignKey("PhaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Phase"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSection", b => + { + b.HasOne("GozareshgirProgramManager.Domain.SkillAgg.Entities.Skill", "Skill") + .WithMany("Sections") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectTask", "Task") + .WithMany("Sections") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Skill"); + + b.Navigation("Task"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSectionActivity", b => + { + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSection", "Section") + .WithMany("Activities") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSectionAdditionalTime", b => + { + b.HasOne("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSection", null) + .WithMany("AdditionalTimes") + .HasForeignKey("TaskSectionId"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.RoleAgg.Entities.Role", b => + { + b.OwnsMany("GozareshgirProgramManager.Domain.PermissionAgg.Entities.Permission", "Permissions", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Code") + .HasColumnType("int"); + + b1.Property("RoleId") + .HasColumnType("bigint"); + + b1.HasKey("Id"); + + b1.HasIndex("RoleId"); + + b1.ToTable("PmRolePermissions", (string)null); + + b1.WithOwner("Role") + .HasForeignKey("RoleId"); + + b1.Navigation("Role"); + }); + + b.Navigation("Permissions"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities.SalaryPaymentSetting", b => + { + b.OwnsMany("GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities.WorkingHours", "WorkingHoursList", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("EndShiftOne") + .HasColumnType("time(0)"); + + b1.Property("EndShiftTwo") + .HasColumnType("time(0)"); + + b1.Property("HasRestTime") + .HasColumnType("bit"); + + b1.Property("HasShiftOne") + .HasColumnType("bit"); + + b1.Property("HasShiftTow") + .HasColumnType("bit"); + + b1.Property("IsActiveDay") + .HasColumnType("bit"); + + b1.Property("PersianDayOfWeek") + .HasColumnType("int"); + + b1.Property("RestTime") + .HasColumnType("time(0)"); + + b1.Property("SalaryPaymentSettingId") + .HasColumnType("bigint"); + + b1.Property("ShiftDurationInMinutes") + .HasColumnType("int"); + + b1.Property("StartShiftOne") + .HasColumnType("time(0)"); + + b1.Property("StartShiftTwo") + .HasColumnType("time(0)"); + + b1.HasKey("Id"); + + b1.HasIndex("SalaryPaymentSettingId"); + + b1.ToTable("WorkingHours", (string)null); + + b1.WithOwner("SalaryPaymentSetting") + .HasForeignKey("SalaryPaymentSettingId"); + + b1.Navigation("SalaryPaymentSetting"); + }); + + 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 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("RoleId") + .HasColumnType("bigint"); + + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.HasKey("Id"); + + b1.HasIndex("UserId"); + + b1.ToTable("RoleUsers", (string)null); + + b1.WithOwner("User") + .HasForeignKey("UserId"); + + b1.Navigation("User"); + }); + + b.Navigation("RoleUser"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.UserRefreshToken", b => + { + b.HasOne("GozareshgirProgramManager.Domain.UserAgg.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.Project", b => + { + b.Navigation("Phases"); + + b.Navigation("ProjectSections"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectPhase", b => + { + b.Navigation("PhaseSections"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.ProjectTask", b => + { + b.Navigation("Sections"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.ProjectAgg.Entities.TaskSection", b => + { + b.Navigation("Activities"); + + b.Navigation("AdditionalTimes"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.SkillAgg.Entities.Skill", b => + { + b.Navigation("Sections"); + }); + + modelBuilder.Entity("GozareshgirProgramManager.Domain.UserAgg.Entities.User", b => + { + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/20260105112925_add task chat - uploaded file.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/20260105112925_add task chat - uploaded file.cs new file mode 100644 index 00000000..65c9ed07 --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/20260105112925_add task chat - uploaded file.cs @@ -0,0 +1,158 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GozareshgirProgramManager.Infrastructure.Migrations +{ + /// + public partial class addtaskchatuploadedfile : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TaskChatMessages", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TaskId = table.Column(type: "uniqueidentifier", nullable: false), + SenderUserId = table.Column(type: "bigint", nullable: false), + MessageType = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + TextContent = table.Column(type: "nvarchar(4000)", maxLength: 4000, nullable: true), + FileId = table.Column(type: "uniqueidentifier", nullable: true), + ReplyToMessageId = table.Column(type: "uniqueidentifier", nullable: true), + IsEdited = table.Column(type: "bit", nullable: false, defaultValue: false), + EditedDate = table.Column(type: "datetime2", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeletedDate = table.Column(type: "datetime2", nullable: true), + IsPinned = table.Column(type: "bit", nullable: false, defaultValue: false), + PinnedDate = table.Column(type: "datetime2", nullable: true), + PinnedByUserId = table.Column(type: "bigint", nullable: true), + CreationDate = table.Column(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(type: "uniqueidentifier", nullable: false), + OriginalFileName = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + UniqueFileName = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + FileExtension = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + FileSizeBytes = table.Column(type: "bigint", nullable: false), + MimeType = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + FileType = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Category = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + StorageProvider = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + StoragePath = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + StorageUrl = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + ThumbnailUrl = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + UploadedByUserId = table.Column(type: "bigint", nullable: false), + UploadDate = table.Column(type: "datetime2", nullable: false), + Status = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + ImageWidth = table.Column(type: "int", nullable: true), + ImageHeight = table.Column(type: "int", nullable: true), + DurationSeconds = table.Column(type: "int", nullable: true), + VirusScanDate = table.Column(type: "datetime2", nullable: true), + IsVirusScanPassed = table.Column(type: "bit", nullable: true), + VirusScanResult = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeletedDate = table.Column(type: "datetime2", nullable: true), + DeletedByUserId = table.Column(type: "bigint", nullable: true), + ReferenceEntityType = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + ReferenceEntityId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + CreationDate = table.Column(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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TaskChatMessages"); + + migrationBuilder.DropTable( + name: "UploadedFiles"); + } + } +} diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/AppDbContextModelSnapshot.cs index 4382f61b..2ac44b92 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/AppDbContextModelSnapshot.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -102,6 +102,131 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations b.ToTable("Customers", (string)null); }); + modelBuilder.Entity("GozareshgirProgramManager.Domain.FileManagementAgg.Entities.UploadedFile", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DeletedByUserId") + .HasColumnType("bigint"); + + b.Property("DeletedDate") + .HasColumnType("datetime2"); + + b.Property("DurationSeconds") + .HasColumnType("int"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ImageHeight") + .HasColumnType("int"); + + b.Property("ImageWidth") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsVirusScanPassed") + .HasColumnType("bit"); + + b.Property("MimeType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ReferenceEntityId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ReferenceEntityType") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("StoragePath") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("StorageProvider") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("StorageUrl") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ThumbnailUrl") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("UniqueFileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UploadDate") + .HasColumnType("datetime2"); + + b.Property("UploadedByUserId") + .HasColumnType("bigint"); + + b.Property("VirusScanDate") + .HasColumnType("datetime2"); + + b.Property("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("Id") @@ -495,6 +620,81 @@ namespace GozareshgirProgramManager.Infrastructure.Migrations b.ToTable("Skills", (string)null); }); + modelBuilder.Entity("GozareshgirProgramManager.Domain.TaskChatAgg.Entities.TaskChatMessage", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DeletedDate") + .HasColumnType("datetime2"); + + b.Property("EditedDate") + .HasColumnType("datetime2"); + + b.Property("FileId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsEdited") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsPinned") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("PinnedByUserId") + .HasColumnType("bigint"); + + b.Property("PinnedDate") + .HasColumnType("datetime2"); + + b.Property("ReplyToMessageId") + .HasColumnType("uniqueidentifier"); + + b.Property("SenderUserId") + .HasColumnType("bigint"); + + b.Property("TaskId") + .HasColumnType("uniqueidentifier"); + + b.Property("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("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 => diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Context/ProgramManagerDbContext.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Context/ProgramManagerDbContext.cs index 2fc4854c..01979e6f 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Context/ProgramManagerDbContext.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Context/ProgramManagerDbContext.cs @@ -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 Roles { get; set; } = null!; public DbSet Skills { get; set; } = null!; + + // File Management + public DbSet UploadedFiles { get; set; } = null!; + + // Task Chat + public DbSet TaskChatMessages { get; set; } = null!; + protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(ProgramManagerDbContext).Assembly); diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Mappings/TaskChatMessageMapping.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Mappings/TaskChatMessageMapping.cs new file mode 100644 index 00000000..5675bb2d --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Mappings/TaskChatMessageMapping.cs @@ -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 +{ + public void Configure(EntityTypeBuilder 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() + .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); + } +} diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Mappings/UploadedFileMapping.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Mappings/UploadedFileMapping.cs new file mode 100644 index 00000000..f3ed3539 --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Mappings/UploadedFileMapping.cs @@ -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 +{ + public void Configure(EntityTypeBuilder 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() + .HasMaxLength(50); + + builder.Property(x => x.Category) + .IsRequired() + .HasConversion() + .HasMaxLength(100); + + // ذخیره‌سازی + builder.Property(x => x.StorageProvider) + .IsRequired() + .HasConversion() + .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() + .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); + } +} diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/FileManagement/UploadedFileRepository.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/FileManagement/UploadedFileRepository.cs new file mode 100644 index 00000000..495ae321 --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/FileManagement/UploadedFileRepository.cs @@ -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 GetByIdAsync(Guid fileId) + { + return await _context.UploadedFiles + .FirstOrDefaultAsync(x => x.Id == fileId); + } + + public async Task GetByUniqueFileNameAsync(string uniqueFileName) + { + return await _context.UploadedFiles + .FirstOrDefaultAsync(x => x.UniqueFileName == uniqueFileName); + } + + public async Task> 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> 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> 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> 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> 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 GetUserFilesCountAsync(long userId) + { + return await _context.UploadedFiles + .CountAsync(x => x.UploadedByUserId == userId); + } + + public async Task GetUserTotalFileSizeAsync(long userId) + { + return await _context.UploadedFiles + .Where(x => x.UploadedByUserId == userId) + .SumAsync(x => x.FileSizeBytes); + } + + public async Task> GetExpiredFilesAsync(DateTime olderThan) + { + return await _context.UploadedFiles + .IgnoreQueryFilters() // Include deleted files + .Where(x => x.IsDeleted && x.DeletedDate < olderThan) + .ToListAsync(); + } + + public async Task 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 SaveChangesAsync() + { + return await _context.SaveChangesAsync(); + } + + public async Task ExistsAsync(Guid fileId) + { + return await _context.UploadedFiles + .AnyAsync(x => x.Id == fileId); + } + + public async Task ExistsByUniqueFileNameAsync(string uniqueFileName) + { + return await _context.UploadedFiles + .AnyAsync(x => x.UniqueFileName == uniqueFileName); + } +} + diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskChat/TaskChatMessageRepository.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskChat/TaskChatMessageRepository.cs new file mode 100644 index 00000000..f6b0e87d --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskChat/TaskChatMessageRepository.cs @@ -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 GetByIdAsync(Guid messageId) + { + return await _context.TaskChatMessages + .Include(x => x.ReplyToMessage) + .FirstOrDefaultAsync(x => x.Id == messageId); + } + + public async Task> 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 GetTaskMessageCountAsync(Guid taskId) + { + return await _context.TaskChatMessages + .CountAsync(x => x.TaskId == taskId); + } + + public async Task> 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 GetLastMessageAsync(Guid taskId) + { + return await _context.TaskChatMessages + .Where(x => x.TaskId == taskId) + .OrderByDescending(x => x.CreationDate) + .FirstOrDefaultAsync(); + } + + public async Task> 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> 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> 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 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 SaveChangesAsync() + { + return await _context.SaveChangesAsync(); + } + + public async Task ExistsAsync(Guid messageId) + { + return await _context.TaskChatMessages + .AnyAsync(x => x.Id == messageId); + } +} + diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/LocalFileStorageService.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/LocalFileStorageService.cs new file mode 100644 index 00000000..dd2b8c8a --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/LocalFileStorageService.cs @@ -0,0 +1,108 @@ +using GozareshgirProgramManager.Application.Services.FileManagement; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement; + +/// +/// سرویس ذخیره‌سازی فایل در سیستم فایل محلی +/// +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}/uploads"; + } + else + { + _baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/uploads"; + } + + // ایجاد پوشه اگر وجود نداشت + 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 GetFileStreamAsync(string storagePath) + { + if (!File.Exists(storagePath)) + { + return Task.FromResult(null); + } + + var stream = new FileStream(storagePath, FileMode.Open, FileAccess.Read, FileShare.Read); + return Task.FromResult(stream); + } + + public Task ExistsAsync(string storagePath) + { + return Task.FromResult(File.Exists(storagePath)); + } + + public string GetFileUrl(string storagePath) + { + var relativePath = Path.GetRelativePath(_uploadBasePath, storagePath) + .Replace("\\", "/"); + return $"{_baseUrl}/{relativePath}"; + } +} diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/ThumbnailGeneratorService.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/ThumbnailGeneratorService.cs new file mode 100644 index 00000000..df55c827 --- /dev/null +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Services/FileManagement/ThumbnailGeneratorService.cs @@ -0,0 +1,111 @@ +using GozareshgirProgramManager.Application.Services.FileManagement; +using Microsoft.Extensions.Configuration; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Formats.Jpeg; + +namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement; + +/// +/// سرویس تولید thumbnail با استفاده از ImageSharp +/// +public class ThumbnailGeneratorService : IThumbnailGeneratorService +{ + private readonly string _thumbnailBasePath; + private readonly string _baseUrl; + + public ThumbnailGeneratorService(IConfiguration configuration) + { + _thumbnailBasePath = configuration["FileStorage:ThumbnailPath"] + ?? Path.Combine(Directory.GetCurrentDirectory(), "Uploads", "Thumbnails"); + + _baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/uploads"; + + if (!Directory.Exists(_thumbnailBasePath)) + { + Directory.CreateDirectory(_thumbnailBasePath); + } + } + + public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync( + string imagePath, + 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}"; + + // مسیر ذخیره thumbnail + var thumbnailPath = Path.Combine(_thumbnailBasePath, thumbnailFileName); + + // ذخیره thumbnail با کیفیت 80 + await image.SaveAsync(thumbnailPath, new JpegEncoder { Quality = 80 }); + + // URL thumbnail + var thumbnailUrl = $"{_baseUrl}/thumbnails/{thumbnailFileName}"; + + return (thumbnailPath, thumbnailUrl); + } + catch (Exception) + { + // در صورت خطا null برمی‌گردانیم + return null; + } + } + + public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(string videoPath) + { + // 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; + } + } +} + diff --git a/ServiceHost/Areas/Admin/Controllers/ProgramManager/TaskChatController.cs b/ServiceHost/Areas/Admin/Controllers/ProgramManager/TaskChatController.cs new file mode 100644 index 00000000..39983e00 --- /dev/null +++ b/ServiceHost/Areas/Admin/Controllers/ProgramManager/TaskChatController.cs @@ -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; + +/// +/// کنترلر مدیریت چت تسک +/// +public class TaskChatController : ProgramManagerBaseController +{ + private readonly IMediator _mediator; + + public TaskChatController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// دریافت لیست پیام‌های یک تسک + /// + /// شناسه تسک + /// صفحه (پیش‌فرض: 1) + /// تعداد در هر صفحه (پیش‌فرض: 50) + [HttpGet("{taskId:guid}/messages")] + public async Task>>> 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; + } + + /// + /// دریافت پیام‌های پین شده یک تسک + /// + /// شناسه تسک + [HttpGet("{taskId:guid}/messages/pinned")] + public async Task>>> GetPinnedMessages(Guid taskId) + { + var query = new GetPinnedMessagesQuery(taskId); + var result = await _mediator.Send(query); + return result; + } + + /// + /// جستجو در پیام‌های یک تسک + /// + /// شناسه تسک + /// متن جستجو + /// صفحه + /// تعداد در هر صفحه + [HttpGet("{taskId:guid}/messages/search")] + public async Task>>> 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; + } + + /// + /// ارسال پیام جدید (با یا بدون فایل) + /// + [HttpPost("messages")] + public async Task>> SendMessage( + [FromForm] SendMessageCommand command) + { + var result = await _mediator.Send(command); + return result; + } + + /// + /// ویرایش پیام (فقط متن) + /// + /// شناسه پیام + /// محتوای جدید + [HttpPut("messages/{messageId:guid}")] + public async Task> EditMessage( + Guid messageId, + [FromBody] EditMessageRequest request) + { + var command = new EditMessageCommand(messageId, request.NewTextContent); + var result = await _mediator.Send(command); + return result; + } + + /// + /// حذف پیام + /// + /// شناسه پیام + [HttpDelete("messages/{messageId:guid}")] + public async Task> DeleteMessage(Guid messageId) + { + var command = new DeleteMessageCommand(messageId); + var result = await _mediator.Send(command); + return result; + } + + /// + /// پین کردن پیام + /// + /// شناسه پیام + [HttpPost("messages/{messageId:guid}/pin")] + public async Task> PinMessage(Guid messageId) + { + var command = new PinMessageCommand(messageId); + var result = await _mediator.Send(command); + return result; + } + + /// + /// برداشتن پین پیام + /// + /// شناسه پیام + [HttpPost("messages/{messageId:guid}/unpin")] + public async Task> 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; +} + diff --git a/ServiceHost/Program.cs b/ServiceHost/Program.cs index 73ff3d88..76189996 100644 --- a/ServiceHost/Program.cs +++ b/ServiceHost/Program.cs @@ -492,6 +492,24 @@ app.UseHttpsRedirection(); app.UseStaticFiles(); +// Static files برای فایل‌های آپلود شده +var uploadsPath = builder.Configuration["FileStorage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "Uploads"); +if (!Directory.Exists(uploadsPath)) +{ + Directory.CreateDirectory(uploadsPath); +} + +app.UseStaticFiles(new StaticFileOptions +{ + FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(uploadsPath), + RequestPath = "/uploads", + OnPrepareResponse = ctx => + { + // Cache برای فایل‌ها (30 روز) + ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000"); + } +}); + app.UseCookiePolicy(); diff --git a/ServiceHost/appsettings.Development.json b/ServiceHost/appsettings.Development.json index d892b51d..cfcfcc1e 100644 --- a/ServiceHost/appsettings.Development.json +++ b/ServiceHost/appsettings.Development.json @@ -64,8 +64,14 @@ "Issuer": "GozareshgirApp", "Audience": "GozareshgirUsers", "ExpirationMinutes": 30 + }, + "FileStorage": { + "BaseUrl": "https://localhost:7032/uploads", + "MaxFileSizeBytes": 104857600, + "AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".gif", ".webp" ], + "AllowedVideoExtensions": [ ".mp4", ".avi", ".mov", ".wmv" ], + "AllowedAudioExtensions": [ ".mp3", ".wav", ".ogg", ".m4a" ], + "AllowedDocumentExtensions": [ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt" ] } - } - diff --git a/ServiceHost/appsettings.json b/ServiceHost/appsettings.json index e5286144..c3080636 100644 --- a/ServiceHost/appsettings.json +++ b/ServiceHost/appsettings.json @@ -50,5 +50,13 @@ "Issuer": "GozareshgirApp", "Audience": "GozareshgirUsers", "ExpirationMinutes": 30 + }, + "FileStorage": { + "BaseUrl": "https://api.pm.gozareshgir.ir/uploads", + "MaxFileSizeBytes": 104857600, + "AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".gif", ".webp" ], + "AllowedVideoExtensions": [ ".mp4", ".avi", ".mov", ".wmv" ], + "AllowedAudioExtensions": [ ".mp3", ".wav", ".ogg", ".m4a" ], + "AllowedDocumentExtensions": [ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt" ] } }