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