diff --git a/.gitignore b/.gitignore
index aa9d4d68..89d3b553 100644
--- a/.gitignore
+++ b/.gitignore
@@ -364,3 +364,7 @@ MigrationBackup/
.idea
/ServiceHost/appsettings.Development.json
/ServiceHost/appsettings.json
+
+# Storage folder - ignore all uploaded files, thumbnails, and temporary files
+ServiceHost/Storage
+
diff --git a/DadmehrGostar.sln b/DadmehrGostar.sln
index 342cf0ef..c39485ab 100644
--- a/DadmehrGostar.sln
+++ b/DadmehrGostar.sln
@@ -89,6 +89,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackgroundInstitutionContract.Task", "BackgroundInstitutionContract\BackgroundInstitutionContract.Task\BackgroundInstitutionContract.Task.csproj", "{F78FBB92-294B-88BA-168D-F0C578B0D7D6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProgramManager", "ProgramManager", "{67AFF7B6-4C4F-464C-A90D-9BDB644D83A9}"
+ ProjectSection(SolutionItems) = preProject
+ ProgramManager\appsettings.FileStorage.json = ProgramManager\appsettings.FileStorage.json
+ EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{48F6F6A5-7340-42F8-9216-BEB7A4B7D5A1}"
EndProject
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/Projects/Commands/AddTaskToPhase/AddTaskToPhaseCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AddTaskToPhase/AddTaskToPhaseCommand.cs
index ba82f57c..1373f8a6 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AddTaskToPhase/AddTaskToPhaseCommand.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/AddTaskToPhase/AddTaskToPhaseCommand.cs
@@ -10,7 +10,7 @@ public record AddTaskToPhaseCommand(
Guid PhaseId,
string Name,
string? Description = null,
- TaskPriority Priority = TaskPriority.Medium,
+ ProjectTaskPriority Priority = ProjectTaskPriority.Medium,
int OrderIndex = 0,
DateTime? DueDate = null
) : IBaseCommand;
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeTaskPriority/ChangeTaskPriorityCommand.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeTaskPriority/ChangeTaskPriorityCommand.cs
index 23d23830..d6ea7e1e 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeTaskPriority/ChangeTaskPriorityCommand.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeTaskPriority/ChangeTaskPriorityCommand.cs
@@ -6,35 +6,103 @@ using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
namespace GozareshgirProgramManager.Application.Modules.Projects.Commands.ChangeTaskPriority;
-///
-/// Command to change a task priority.
-///
-public record ChangeTaskPriorityCommand(Guid TaskId, TaskPriority Priority) : IBaseCommand;
+public record ChangeTaskPriorityCommand(
+ Guid Id,
+ ProjectHierarchyLevel Level,
+ ProjectTaskPriority Priority
+) : IBaseCommand;
public class ChangeTaskPriorityCommandHandler : IBaseCommandHandler
{
private readonly IProjectTaskRepository _taskRepository;
+ private readonly IProjectPhaseRepository _phaseRepository;
+ private readonly IProjectRepository _projectRepository;
private readonly IUnitOfWork _unitOfWork;
- public ChangeTaskPriorityCommandHandler(IProjectTaskRepository taskRepository, IUnitOfWork unitOfWork)
+ public ChangeTaskPriorityCommandHandler(
+ IProjectTaskRepository taskRepository,
+ IProjectPhaseRepository phaseRepository,
+ IProjectRepository projectRepository,
+ IUnitOfWork unitOfWork)
{
_taskRepository = taskRepository;
+ _phaseRepository = phaseRepository;
+ _projectRepository = projectRepository;
_unitOfWork = unitOfWork;
}
public async Task Handle(ChangeTaskPriorityCommand request, CancellationToken cancellationToken)
{
- var task = await _taskRepository.GetByIdAsync(request.TaskId, cancellationToken);
+ switch (request.Level)
+ {
+ case ProjectHierarchyLevel.Task:
+ return await HandleTaskLevelAsync(request.Id, request.Priority, cancellationToken);
+ case ProjectHierarchyLevel.Phase:
+ return await HandlePhaseLevelAsync(request.Id, request.Priority, cancellationToken);
+ case ProjectHierarchyLevel.Project:
+ return await HandleProjectLevelAsync(request.Id, request.Priority, cancellationToken);
+ default:
+ return OperationResult.Failure("سطح نامعتبر است");
+ }
+ }
+
+ // Task-level priority update
+ private async Task HandleTaskLevelAsync(Guid taskId, ProjectTaskPriority priority, CancellationToken ct)
+ {
+ var task = await _taskRepository.GetByIdAsync(taskId, ct);
if (task is null)
return OperationResult.NotFound("تسک یافت نشد");
- // Idempotent: if already same priority, skip extra work
- if (task.Priority != request.Priority)
+ if (task.Priority != priority)
{
- task.SetPriority(request.Priority);
+ task.SetPriority(priority);
}
- await _unitOfWork.SaveChangesAsync(cancellationToken);
+ await _unitOfWork.SaveChangesAsync(ct);
+ return OperationResult.Success();
+ }
+
+ // Phase-level bulk priority update
+ private async Task HandlePhaseLevelAsync(Guid phaseId, ProjectTaskPriority priority, CancellationToken ct)
+ {
+ var phase = await _phaseRepository.GetWithTasksAsync(phaseId);
+ if (phase is null)
+ return OperationResult.NotFound("فاز یافت نشد");
+
+ var tasks = phase.Tasks?.ToList() ?? new List();
+ foreach (var t in tasks)
+ {
+ if (t.Priority != priority)
+ {
+ t.SetPriority(priority);
+ }
+ }
+
+ await _unitOfWork.SaveChangesAsync(ct);
+ return OperationResult.Success();
+ }
+
+ // Project-level bulk priority update across all phases
+ private async Task HandleProjectLevelAsync(Guid projectId, ProjectTaskPriority priority, CancellationToken ct)
+ {
+ var project = await _projectRepository.GetWithFullHierarchyAsync(projectId);
+ if (project is null)
+ return OperationResult.NotFound("پروژه یافت نشد");
+
+ var phases = project.Phases?.ToList() ?? new List();
+ foreach (var phase in phases)
+ {
+ var tasks = phase.Tasks?.ToList() ?? new List();
+ foreach (var t in tasks)
+ {
+ if (t.Priority != priority)
+ {
+ t.SetPriority(priority);
+ }
+ }
+ }
+
+ await _unitOfWork.SaveChangesAsync(ct);
return OperationResult.Success();
}
}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetTaskListDto.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetTaskListDto.cs
index 62b7e26e..0661cdb1 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetTaskListDto.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/GetProjectsList/GetTaskListDto.cs
@@ -15,7 +15,7 @@ public class GetTaskDto
// Task-specific fields
public TimeSpan SpentTime { get; init; }
public TimeSpan RemainingTime { get; init; }
- public TaskPriority Priority { get; set; }
+ public ProjectTaskPriority Priority { get; set; }
public List Sections { get; init; }
}
diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListQueryHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListQueryHandler.cs
index a082f648..f23ea7d5 100644
--- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListQueryHandler.cs
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Queries/ProjectBoardList/ProjectBoardListQueryHandler.cs
@@ -105,6 +105,7 @@ public class ProjectBoardListQueryHandler : IBaseQueryHandler
+{
+ private readonly ITaskChatMessageRepository _repository;
+ private readonly IAuthHelper _authHelper;
+
+ public DeleteMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
+ {
+ _repository = repository;
+ _authHelper = authHelper;
+ }
+
+ public async Task Handle(DeleteMessageCommand request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId()??
+ throw new UnAuthorizedException("کاربر احراز هویت نشده است");
+
+ var message = await _repository.GetByIdAsync(request.MessageId);
+ if (message == null)
+ {
+ return OperationResult.NotFound("پیام یافت نشد");
+ }
+
+ try
+ {
+ message.DeleteMessage(currentUserId);
+ await _repository.UpdateAsync(message);
+ await _repository.SaveChangesAsync();
+
+ // TODO: SignalR notification
+
+ return OperationResult.Success();
+ }
+ catch (Exception ex)
+ {
+ return OperationResult.ValidationError(ex.Message);
+ }
+ }
+}
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..2066a769
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/EditMessage/EditMessageCommand.cs
@@ -0,0 +1,51 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
+using MediatR;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.EditMessage;
+
+public record EditMessageCommand(
+ Guid MessageId,
+ string NewTextContent
+) : IBaseCommand;
+
+public class EditMessageCommandHandler : IBaseCommandHandler
+{
+ private readonly ITaskChatMessageRepository _repository;
+ private readonly IAuthHelper _authHelper;
+
+ public EditMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
+ {
+ _repository = repository;
+ _authHelper = authHelper;
+ }
+
+ public async Task Handle(EditMessageCommand request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId()??
+ throw new UnAuthorizedException("کاربر احراز هویت نشده است");
+
+ var message = await _repository.GetByIdAsync(request.MessageId);
+ if (message == null)
+ {
+ return OperationResult.NotFound("پیام یافت نشد");
+ }
+
+ try
+ {
+ message.EditMessage(request.NewTextContent, currentUserId);
+ await _repository.UpdateAsync(message);
+ await _repository.SaveChangesAsync();
+
+ // TODO: SignalR notification
+
+ return OperationResult.Success();
+ }
+ catch (Exception ex)
+ {
+ return OperationResult.ValidationError(ex.Message);
+ }
+ }
+}
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..9841df53
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/PinMessage/PinMessageCommand.cs
@@ -0,0 +1,46 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
+using MediatR;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.PinMessage;
+
+public record PinMessageCommand(Guid MessageId) : IBaseCommand;
+
+public class PinMessageCommandHandler : IBaseCommandHandler
+{
+ private readonly ITaskChatMessageRepository _repository;
+ private readonly IAuthHelper _authHelper;
+
+ public PinMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
+ {
+ _repository = repository;
+ _authHelper = authHelper;
+ }
+
+ public async Task Handle(PinMessageCommand request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId()??
+ throw new UnAuthorizedException("کاربر احراز هویت نشده است");
+
+ var message = await _repository.GetByIdAsync(request.MessageId);
+ if (message == null)
+ {
+ return OperationResult.NotFound("پیام یافت نشد");
+ }
+
+ try
+ {
+ message.PinMessage(currentUserId);
+ await _repository.UpdateAsync(message);
+ await _repository.SaveChangesAsync();
+
+ return OperationResult.Success();
+ }
+ catch (Exception ex)
+ {
+ return OperationResult.ValidationError(ex.Message);
+ }
+ }
+}
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..245d8b39
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/SendMessage/SendMessageCommand.cs
@@ -0,0 +1,212 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
+using GozareshgirProgramManager.Application.Services.FileManagement;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
+using GozareshgirProgramManager.Domain.FileManagementAgg.Entities;
+using GozareshgirProgramManager.Domain.FileManagementAgg.Repositories;
+using GozareshgirProgramManager.Domain.FileManagementAgg.Enums;
+using GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
+using MediatR;
+using Microsoft.AspNetCore.Http;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.SendMessage;
+
+public record SendMessageCommand(
+ Guid TaskId,
+ MessageType MessageType,
+ string? TextContent,
+ IFormFile? File,
+ Guid? ReplyToMessageId
+) : IBaseCommand;
+
+public class SendMessageCommandHandler : IBaseCommandHandler
+{
+ private readonly ITaskChatMessageRepository _messageRepository;
+ private readonly IUploadedFileRepository _fileRepository;
+ private readonly IProjectTaskRepository _taskRepository;
+ private readonly IFileStorageService _fileStorageService;
+ private readonly IThumbnailGeneratorService _thumbnailService;
+ private readonly IAuthHelper _authHelper;
+
+ public SendMessageCommandHandler(
+ ITaskChatMessageRepository messageRepository,
+ IUploadedFileRepository fileRepository,
+ IProjectTaskRepository taskRepository,
+ IFileStorageService fileStorageService,
+ IThumbnailGeneratorService thumbnailService, IAuthHelper authHelper)
+ {
+ _messageRepository = messageRepository;
+ _fileRepository = fileRepository;
+ _taskRepository = taskRepository;
+ _fileStorageService = fileStorageService;
+ _thumbnailService = thumbnailService;
+ _authHelper = authHelper;
+ }
+
+ public async Task> Handle(SendMessageCommand request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId()
+ ?? throw new UnAuthorizedException("کاربر احراز هویت نشده است");
+
+ var task = await _taskRepository.GetByIdAsync(request.TaskId, cancellationToken);
+ if (task == null)
+ {
+ return OperationResult.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, category: "TaskChatMessage");
+ if (thumbnail.HasValue)
+ {
+ uploadedFile.SetThumbnail(thumbnail.Value.ThumbnailUrl);
+ }
+ }
+
+ await _fileRepository.UpdateAsync(uploadedFile);
+ await _fileRepository.SaveChangesAsync();
+
+ uploadedFileId = uploadedFile.Id;
+ }
+ catch (Exception ex)
+ {
+ await _fileRepository.DeleteAsync(uploadedFile);
+ await _fileRepository.SaveChangesAsync();
+
+ return OperationResult.ValidationError($"خطا در آپلود فایل: {ex.Message}");
+ }
+ }
+
+ var message = new TaskChatMessage(
+ taskId: request.TaskId,
+ senderUserId: currentUserId,
+ messageType: request.MessageType,
+ textContent: request.TextContent,
+ uploadedFileId
+ );
+
+ if (request.ReplyToMessageId.HasValue)
+ {
+ message.SetReplyTo(request.ReplyToMessageId.Value);
+ }
+
+ await _messageRepository.AddAsync(message);
+ await _messageRepository.SaveChangesAsync();
+
+ if (uploadedFileId.HasValue)
+ {
+ var file = await _fileRepository.GetByIdAsync(uploadedFileId.Value);
+ if (file != null)
+ {
+ file.SetReference("TaskChatMessage", message.Id.ToString());
+ await _fileRepository.UpdateAsync(file);
+ await _fileRepository.SaveChangesAsync();
+ }
+ }
+
+ var dto = new MessageDto
+ {
+ Id = message.Id,
+ TaskId = message.TaskId,
+ SenderUserId = message.SenderUserId,
+ SenderName = "کاربر",
+ MessageType = message.MessageType.ToString(),
+ TextContent = message.TextContent,
+ ReplyToMessageId = message.ReplyToMessageId,
+ IsEdited = message.IsEdited,
+ IsPinned = message.IsPinned,
+ CreationDate = message.CreationDate,
+ IsMine = true
+ };
+
+ if (uploadedFileId.HasValue)
+ {
+ var file = await _fileRepository.GetByIdAsync(uploadedFileId.Value);
+ if (file != null)
+ {
+ dto.File = new MessageFileDto
+ {
+ Id = file.Id,
+ FileName = file.OriginalFileName,
+ FileUrl = file.StorageUrl ?? "",
+ FileSizeBytes = file.FileSizeBytes,
+ FileType = file.FileType.ToString(),
+ ThumbnailUrl = file.ThumbnailUrl,
+ ImageWidth = file.ImageWidth,
+ ImageHeight = file.ImageHeight,
+ DurationSeconds = file.DurationSeconds
+ };
+ }
+ }
+
+ return OperationResult.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..664087cd
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Commands/UnpinMessage/UnpinMessageCommand.cs
@@ -0,0 +1,46 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
+using MediatR;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Commands.UnpinMessage;
+
+public record UnpinMessageCommand(Guid MessageId) : IBaseCommand;
+
+public class UnpinMessageCommandHandler : IBaseCommandHandler
+{
+ private readonly ITaskChatMessageRepository _repository;
+ private readonly IAuthHelper _authHelper;
+
+ public UnpinMessageCommandHandler(ITaskChatMessageRepository repository, IAuthHelper authHelper)
+ {
+ _repository = repository;
+ _authHelper = authHelper;
+ }
+
+ public async Task Handle(UnpinMessageCommand request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _authHelper.GetCurrentUserId()??
+ throw new UnAuthorizedException("کاربر احراز هویت نشده است");
+
+ var message = await _repository.GetByIdAsync(request.MessageId);
+ if (message == null)
+ {
+ return OperationResult.NotFound("پیام یافت نشد");
+ }
+
+ try
+ {
+ message.UnpinMessage(currentUserId);
+ await _repository.UpdateAsync(message);
+ await _repository.SaveChangesAsync();
+
+ return OperationResult.Success();
+ }
+ catch (Exception ex)
+ {
+ return OperationResult.ValidationError(ex.Message);
+ }
+ }
+}
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..fbc3e3b8
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetMessages/GetMessagesQuery.cs
@@ -0,0 +1,187 @@
+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);
+
+ // ✅ گرفتن تمامی کاربران برای نمایش نام کامل فرستنده به جای "کاربر"
+ // این بخش تمام UserId هایی که در پیامها استفاده شده را جمعآوری میکند
+ // و یک Dictionary ایجاد میکند که UserId را به FullName نگاشت میکند
+ var senderUserIds = messages.Select(m => m.SenderUserId).Distinct().ToList();
+ var users = await _context.Users
+ .Where(u => senderUserIds.Contains(u.Id))
+ .ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
+
+ // ✅ گرفتن تمامی زمانهای اضافی (Additional Times) برای نمایش به صورت نوت
+ // در اینجا تمامی TaskSections مربوط به این تسک را میگیریم
+ // و برای هر کدام تمام AdditionalTimes آن را بارگذاری میکنیم
+ var taskSections = await _context.TaskSections
+ .Where(ts => ts.TaskId == request.TaskId)
+ .Include(ts => ts.AdditionalTimes)
+ .ToListAsync(cancellationToken);
+
+ var messageDtos = new List();
+
+ foreach (var message in messages)
+ {
+ // ✅ نام فرستنده را از Dictionary Users بگیر، در صورت عدم وجود "کاربر ناشناس" نمایش بده
+ var senderName = users.ContainsKey(message.SenderUserId)
+ ? users[message.SenderUserId]
+ : "کاربر ناشناس";
+
+ var dto = new MessageDto
+ {
+ Id = message.Id,
+ TaskId = message.TaskId,
+ SenderUserId = message.SenderUserId,
+ SenderName = senderName, // ✅ از User واقعی استفاده میکنیم
+ 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)
+ {
+ // ✅ برای پیامهای Reply نیز نام فرستنده را درست نمایش بده
+ var replySenderName = users.ContainsKey(message.ReplyToMessage.SenderUserId)
+ ? users[message.ReplyToMessage.SenderUserId]
+ : "کاربر ناشناس";
+
+ dto.ReplyToMessage = new MessageDto
+ {
+ Id = message.ReplyToMessage.Id,
+ SenderUserId = message.ReplyToMessage.SenderUserId,
+ SenderName = replySenderName,
+ TextContent = message.ReplyToMessage.TextContent,
+ CreationDate = message.ReplyToMessage.CreationDate
+ };
+ }
+
+ if (message.FileId.HasValue)
+ {
+ var file = await _context.UploadedFiles.FirstOrDefaultAsync(f => f.Id == message.FileId.Value, cancellationToken);
+ if (file != null)
+ {
+ dto.File = new MessageFileDto
+ {
+ Id = file.Id,
+ FileName = file.OriginalFileName,
+ FileUrl = file.StorageUrl ?? "",
+ FileSizeBytes = file.FileSizeBytes,
+ FileType = file.FileType.ToString(),
+ ThumbnailUrl = file.ThumbnailUrl,
+ ImageWidth = file.ImageWidth,
+ ImageHeight = file.ImageHeight,
+ DurationSeconds = file.DurationSeconds
+ };
+ }
+ }
+
+ messageDtos.Add(dto);
+
+ // ✅ اینجا بخش جدید است: نوتهای زمان اضافی را بین پیامها اضافه کن
+ // این بخش تمام AdditionalTimes را که بعد از این پیام اضافه شدهاند را پیدا میکند
+ var additionalTimesAfterMessage = taskSections
+ .SelectMany(ts => ts.AdditionalTimes)
+ .Where(at => at.AddedAt > message.CreationDate) // ✅ تغییر به AddedAt (زمان واقعی اضافه شدن)
+ .OrderBy(at => at.AddedAt)
+ .FirstOrDefault();
+
+ if (additionalTimesAfterMessage != null)
+ {
+ // ✅ تمام AdditionalTimes بین این پیام و پیام قبلی را بگیر
+ var additionalTimesByDate = taskSections
+ .SelectMany(ts => ts.AdditionalTimes)
+ .Where(at => at.AddedAt <= message.CreationDate &&
+ (messageDtos.Count == 1 || at.AddedAt > messageDtos[messageDtos.Count - 2].CreationDate))
+ .OrderBy(at => at.AddedAt)
+ .ToList();
+
+ foreach (var additionalTime in additionalTimesByDate)
+ {
+ // ✅ نام کاربری که این زمان اضافی را اضافه کرد
+ var addedByUserName = additionalTime.AddedByUserId.HasValue && users.TryGetValue(additionalTime.AddedByUserId.Value, out var user)
+ ? user
+ : "سیستم";
+
+ // ✅ محتوای نوت را با اطلاعات کامل ایجاد کن
+ // نمایش میدهد: مقدار زمان + علت + نام کسی که اضافه کرد
+ var noteContent = $"⏱️ زمان اضافی: {additionalTime.Hours.TotalHours:F2} ساعت - {(string.IsNullOrWhiteSpace(additionalTime.Reason) ? "بدون علت" : additionalTime.Reason)} - توسط {addedByUserName}";
+
+ // ✅ نوت را به عنوان MessageDto خاصی ایجاد کن
+ var noteDto = new MessageDto
+ {
+ Id = Guid.NewGuid(),
+ TaskId = request.TaskId,
+ SenderUserId = 0, // ✅ سیستم برای نشان دادن اینکه یک پیام خودکار است
+ SenderName = "سیستم",
+ MessageType = "Note", // ✅ نوع پیام: Note (یادداشت سیستم)
+ TextContent = noteContent,
+ CreationDate = additionalTime.AddedAt, // ✅ تاریخ اضافه شدن زمان اضافی
+ IsMine = false
+ };
+
+ messageDtos.Add(noteDto);
+ }
+ }
+ }
+
+ // ✅ مرتب کردن نهایی تمام پیامها (معمولی + نوتها) بر اساس زمان ایجاد
+ // اینطور که نوتهای زمان اضافی در جای درست خود قرار میگیرند
+ messageDtos = messageDtos.OrderBy(m => m.CreationDate).ToList();
+
+ 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..15e1cf40
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/GetPinnedMessages/GetPinnedMessagesQuery.cs
@@ -0,0 +1,82 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.GetPinnedMessages;
+
+public record GetPinnedMessagesQuery(Guid TaskId) : IBaseQuery>;
+
+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 senderUserIds = messages.Select(m => m.SenderUserId).Distinct().ToList();
+ var users = await _context.Users
+ .Where(u => senderUserIds.Contains(u.Id))
+ .ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
+
+ var messageDtos = new List();
+
+ foreach (var message in messages)
+ {
+ // ✅ نام فرستنده را از User واقعی بگیر (به جای "کاربر" ثابت)
+ var senderName = users.GetValueOrDefault(message.SenderUserId, "کاربر ناشناس");
+
+ var dto = new MessageDto
+ {
+ Id = message.Id,
+ TaskId = message.TaskId,
+ SenderUserId = message.SenderUserId,
+ SenderName = senderName,
+ MessageType = message.MessageType.ToString(),
+ TextContent = message.TextContent,
+ IsPinned = message.IsPinned,
+ PinnedDate = message.PinnedDate,
+ PinnedByUserId = message.PinnedByUserId,
+ CreationDate = message.CreationDate,
+ IsMine = message.SenderUserId == currentUserId
+ };
+
+ if (message.FileId.HasValue)
+ {
+ var file = await _context.UploadedFiles.FirstOrDefaultAsync(f => f.Id == message.FileId.Value, cancellationToken);
+ if (file != null)
+ {
+ dto.File = new MessageFileDto
+ {
+ Id = file.Id,
+ FileName = file.OriginalFileName,
+ FileUrl = file.StorageUrl ?? "",
+ FileSizeBytes = file.FileSizeBytes,
+ FileType = file.FileType.ToString(),
+ ThumbnailUrl = file.ThumbnailUrl
+ };
+ }
+ }
+
+ messageDtos.Add(dto);
+ }
+
+ return OperationResult>.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..dffc1331
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/TaskChat/Queries/SearchMessages/SearchMessagesQuery.cs
@@ -0,0 +1,72 @@
+using GozareshgirProgramManager.Application._Common.Interfaces;
+using GozareshgirProgramManager.Application._Common.Models;
+using GozareshgirProgramManager.Application.Modules.TaskChat.DTOs;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace GozareshgirProgramManager.Application.Modules.TaskChat.Queries.SearchMessages;
+
+public record SearchMessagesQuery(
+ Guid TaskId,
+ string SearchText,
+ int Page = 1,
+ int PageSize = 20
+) : IBaseQuery>;
+
+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 senderUserIds = messages.Select(m => m.SenderUserId).Distinct().ToList();
+ var users = await _context.Users
+ .Where(u => senderUserIds.Contains(u.Id))
+ .ToDictionaryAsync(u => u.Id, u => u.FullName, cancellationToken);
+
+ var messageDtos = new List();
+ foreach (var message in messages)
+ {
+ // ✅ نام فرستنده را از User واقعی بگیر
+ var senderName = users.GetValueOrDefault(message.SenderUserId, "کاربر ناشناس");
+
+ var dto = new MessageDto
+ {
+ Id = message.Id,
+ TaskId = message.TaskId,
+ SenderUserId = message.SenderUserId,
+ SenderName = senderName,
+ MessageType = message.MessageType.ToString(),
+ TextContent = message.TextContent,
+ CreationDate = message.CreationDate,
+ IsMine = message.SenderUserId == currentUserId
+ };
+
+ messageDtos.Add(dto);
+ }
+
+ return OperationResult>.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..cff21a1a
--- /dev/null
+++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Services/FileManagement/IThumbnailGeneratorService.cs
@@ -0,0 +1,34 @@
+namespace GozareshgirProgramManager.Application.Services.FileManagement;
+
+///
+/// سرویس تولید thumbnail برای تصاویر و ویدیوها
+///
+public interface IThumbnailGeneratorService
+{
+ ///
+ /// تولید thumbnail برای تصویر
+ ///
+ Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync(
+ string imagePath,
+ string category,
+ int width = 200,
+ int height = 200);
+
+ ///
+ /// تولید thumbnail برای ویدیو
+ ///
+ Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(
+ string videoPath,
+ string category);
+
+ ///
+ /// حذف 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/ProjectAgg/Entities/ProjectTask.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectTask.cs
index 71de7615..286d0387 100644
--- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectTask.cs
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/ProjectTask.cs
@@ -20,7 +20,7 @@ public class ProjectTask : ProjectHierarchyNode
{
PhaseId = phaseId;
_sections = new List();
- Priority = TaskPriority.Medium;
+ Priority = ProjectTaskPriority.Medium;
AddDomainEvent(new TaskCreatedEvent(Id, phaseId, name));
}
@@ -30,7 +30,7 @@ public class ProjectTask : ProjectHierarchyNode
// Task-specific properties
public Enums.TaskStatus Status { get; private set; } = Enums.TaskStatus.NotStarted;
- public TaskPriority Priority { get; private set; }
+ public ProjectTaskPriority Priority { get; private set; }
public DateTime? StartDate { get; private set; }
public DateTime? EndDate { get; private set; }
public DateTime? DueDate { get; private set; }
@@ -119,7 +119,7 @@ public class ProjectTask : ProjectHierarchyNode
AddDomainEvent(new TaskStatusUpdatedEvent(Id, status));
}
- public void SetPriority(TaskPriority priority)
+ public void SetPriority(ProjectTaskPriority priority)
{
Priority = priority;
AddDomainEvent(new TaskPriorityUpdatedEvent(Id, priority));
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/TaskPriority.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/ProjectTaskPriority.cs
similarity index 92%
rename from ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/TaskPriority.cs
rename to ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/ProjectTaskPriority.cs
index 13ccd784..2a2ab36f 100644
--- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/TaskPriority.cs
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Enums/ProjectTaskPriority.cs
@@ -3,7 +3,7 @@ namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
///
/// اولویت تسک
///
-public enum TaskPriority
+public enum ProjectTaskPriority
{
///
/// پایین
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Events/ProjectEvents.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Events/ProjectEvents.cs
index 155fec66..64f21ef9 100644
--- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Events/ProjectEvents.cs
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Events/ProjectEvents.cs
@@ -78,7 +78,7 @@ public record TaskStatusUpdatedEvent(Guid TaskId, TaskStatus Status) : IDomainEv
public DateTime OccurredOn { get; init; } = DateTime.Now;
}
-public record TaskPriorityUpdatedEvent(Guid TaskId, TaskPriority Priority) : IDomainEvent
+public record TaskPriorityUpdatedEvent(Guid TaskId, ProjectTaskPriority Priority) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.Now;
}
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/IProjectTaskRepository.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/IProjectTaskRepository.cs
index 8b0ff688..9894a764 100644
--- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/IProjectTaskRepository.cs
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/IProjectTaskRepository.cs
@@ -36,7 +36,7 @@ public interface IProjectTaskRepository : IRepository
///
/// Get tasks by priority
///
- Task> GetByPriorityAsync(ProjectAgg.Enums.TaskPriority priority);
+ Task> GetByPriorityAsync(ProjectAgg.Enums.ProjectTaskPriority priority);
///
/// Get tasks assigned to user
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs
new file mode 100644
index 00000000..c937e781
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs
@@ -0,0 +1,183 @@
+using GozareshgirProgramManager.Domain._Common;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Events;
+using MessageType = GozareshgirProgramManager.Domain.TaskChatAgg.Enums.MessageType;
+
+namespace GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
+
+///
+/// پیام چت تسک - Aggregate Root
+/// هر کسی که به تسک دسترسی داشته باشد میتواند پیامها را ببیند و ارسال کند
+/// نیازی به مدیریت گروه و ممبر نیست چون دسترسی از طریق خود تسک کنترل میشود
+///
+public class TaskChatMessage : EntityBase
+{
+ private TaskChatMessage()
+ {
+ }
+
+ public TaskChatMessage(Guid taskId, long senderUserId, MessageType messageType,
+ string? textContent = null,Guid? fileId = null)
+ {
+ TaskId = taskId;
+ SenderUserId = senderUserId;
+ MessageType = messageType;
+ TextContent = textContent;
+ IsEdited = false;
+ IsDeleted = false;
+ IsPinned = false;
+ if (fileId.HasValue)
+ {
+ SetFile(fileId.Value);
+ }
+ ValidateMessage();
+ AddDomainEvent(new TaskChatMessageSentEvent(Id, taskId, senderUserId, messageType));
+ }
+
+ // Reference به Task (Foreign Key فقط - بدون Navigation Property برای جلوگیری از coupling)
+ public Guid TaskId { get; private set; }
+
+ public long SenderUserId { get; private set; }
+
+ public MessageType MessageType { get; private set; }
+
+ // محتوای متنی (برای پیامهای Text و Caption برای فایل/تصویر)
+ public string? TextContent { get; private set; }
+
+ // ارجاع به فایل (برای پیامهای File, Voice, Image, Video)
+ public Guid? FileId { get; private set; }
+
+ // پیام Reply
+ public Guid? ReplyToMessageId { get; private set; }
+ public TaskChatMessage? ReplyToMessage { get; private set; }
+
+ // وضعیت پیام
+ public bool IsEdited { get; private set; }
+ public DateTime? EditedDate { get; private set; }
+ public bool IsDeleted { get; private set; }
+ public DateTime? DeletedDate { get; private set; }
+ public bool IsPinned { get; private set; }
+ public DateTime? PinnedDate { get; private set; }
+ public long? PinnedByUserId { get; private set; }
+
+ private void ValidateMessage()
+ {
+ // ✅ بررسی پیامهای متنی
+ if (MessageType == MessageType.Text && string.IsNullOrWhiteSpace(TextContent))
+ {
+ throw new BadRequestException("پیام متنی نمیتواند خالی باشد");
+ }
+
+ // ✅ بررسی پیامهای فایلی - باید FileId داشته باشند
+ if ((MessageType == MessageType.File || MessageType == MessageType.Voice ||
+ MessageType == MessageType.Image || MessageType == MessageType.Video)
+ && FileId == null)
+ {
+ throw new BadRequestException("پیامهای فایلی باید شناسه فایل داشته باشند");
+ }
+
+ // ✅ بررسی یادداشتهای سیستم - باید محتوای متنی داشته باشند
+ if (MessageType == MessageType.Note && string.IsNullOrWhiteSpace(TextContent))
+ {
+ throw new BadRequestException("یادداشت نمیتواند خالی باشد");
+ }
+ }
+
+ public void SetFile(Guid fileId)
+ {
+ if (MessageType != MessageType.File && MessageType != MessageType.Image &&
+ MessageType != MessageType.Video && MessageType != MessageType.Voice)
+ {
+ throw new BadRequestException("فقط میتوان برای پیامهای فایل، تصویر، ویدیو و صدا شناسه فایل تنظیم کرد");
+ }
+
+ FileId = fileId;
+ }
+
+ public void EditMessage(string newTextContent, long editorUserId)
+ {
+ if (IsDeleted)
+ {
+ throw new BadRequestException("نمیتوان پیام حذف شده را ویرایش کرد");
+ }
+
+ if (editorUserId != SenderUserId)
+ {
+ throw new BadRequestException("فقط فرستنده میتواند پیام را ویرایش کند");
+ }
+
+ if ((MessageType != MessageType.Text && !string.IsNullOrWhiteSpace(TextContent)))
+ {
+ throw new BadRequestException("فقط پیامهای متنی قابل ویرایش هستند");
+ }
+
+ if (string.IsNullOrWhiteSpace(newTextContent))
+ {
+ throw new BadRequestException("محتوای پیام نمیتواند خالی باشد");
+ }
+
+ TextContent = newTextContent;
+ IsEdited = true;
+ EditedDate = DateTime.Now;
+ AddDomainEvent(new TaskChatMessageEditedEvent(Id, TaskId, editorUserId));
+ }
+
+ public void DeleteMessage(long deleterUserId)
+ {
+ if (IsDeleted)
+ {
+ throw new BadRequestException("پیام قبلاً حذف شده است");
+ }
+
+ if (deleterUserId != SenderUserId)
+ {
+ throw new BadRequestException("فقط فرستنده میتواند پیام را حذف کند");
+ }
+
+ IsDeleted = true;
+ DeletedDate = DateTime.Now;
+ AddDomainEvent(new TaskChatMessageDeletedEvent(Id, TaskId, deleterUserId));
+ }
+
+ public void PinMessage(long pinnerUserId)
+ {
+ if (IsDeleted)
+ {
+ throw new BadRequestException("نمیتوان پیام حذف شده را پین کرد");
+ }
+
+ if (IsPinned)
+ {
+ throw new BadRequestException("این پیام قبلاً پین شده است");
+ }
+
+ IsPinned = true;
+ PinnedDate = DateTime.Now;
+ PinnedByUserId = pinnerUserId;
+ AddDomainEvent(new TaskChatMessagePinnedEvent(Id, TaskId, pinnerUserId));
+ }
+
+ public void UnpinMessage(long unpinnerUserId)
+ {
+ if (!IsPinned)
+ {
+ throw new BadRequestException("این پیام پین نشده است");
+ }
+
+ IsPinned = false;
+ PinnedDate = null;
+ PinnedByUserId = null;
+ AddDomainEvent(new TaskChatMessageUnpinnedEvent(Id, TaskId, unpinnerUserId));
+ }
+
+ public void SetReplyTo(Guid replyToMessageId)
+ {
+ ReplyToMessageId = replyToMessageId;
+ }
+
+ public bool IsSentBy(long userId)
+ {
+ return SenderUserId == userId;
+ }
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Enums/MessageType.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Enums/MessageType.cs
new file mode 100644
index 00000000..f35c1034
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Enums/MessageType.cs
@@ -0,0 +1,15 @@
+namespace GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
+
+///
+/// نوع پیام در چت تسک
+///
+public enum MessageType
+{
+ Text = 1, // پیام متنی
+ File = 2, // فایل (اسناد، PDF، و غیره)
+ Image = 3, // تصویر
+ Voice = 4, // پیام صوتی
+ Video = 5, // ویدیو
+ Note = 6, // ✅ یادداشت سیستم (برای زمان اضافی و اطلاعات خودکار)
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Events/TaskChatEvents.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Events/TaskChatEvents.cs
new file mode 100644
index 00000000..9493fa55
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Events/TaskChatEvents.cs
@@ -0,0 +1,31 @@
+using GozareshgirProgramManager.Domain._Common;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
+
+namespace GozareshgirProgramManager.Domain.TaskChatAgg.Events;
+
+// Message Events
+public record TaskChatMessageSentEvent(Guid MessageId, Guid TaskId, long SenderUserId, MessageType MessageType) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record TaskChatMessageEditedEvent(Guid MessageId, Guid TaskId, long EditorUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record TaskChatMessageDeletedEvent(Guid MessageId, Guid TaskId, long DeleterUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record TaskChatMessagePinnedEvent(Guid MessageId, Guid TaskId, long PinnerUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record TaskChatMessageUnpinnedEvent(Guid MessageId, Guid TaskId, long UnpinnerUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs
new file mode 100644
index 00000000..141ebaf1
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs
@@ -0,0 +1,75 @@
+using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
+
+namespace GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
+
+///
+/// Repository برای مدیریت پیامهای چت تسک
+///
+public interface ITaskChatMessageRepository
+{
+ ///
+ /// دریافت پیام بر اساس شناسه
+ ///
+ Task GetByIdAsync(Guid messageId);
+
+ ///
+ /// دریافت لیست پیامهای یک تسک (با صفحهبندی)
+ ///
+ Task> GetTaskMessagesAsync(Guid taskId, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت تعداد کل پیامهای یک تسک
+ ///
+ Task GetTaskMessageCountAsync(Guid taskId);
+
+ ///
+ /// دریافت پیامهای پین شده یک تسک
+ ///
+ Task> GetPinnedMessagesAsync(Guid taskId);
+
+ ///
+ /// دریافت آخرین پیام یک تسک
+ ///
+ Task GetLastMessageAsync(Guid taskId);
+
+ ///
+ /// جستجو در پیامهای یک تسک
+ ///
+ Task> SearchMessagesAsync(Guid taskId, string searchText, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت پیامهای یک کاربر خاص در یک تسک
+ ///
+ Task> GetUserMessagesAsync(Guid taskId, long userId, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت پیامهای با فایل (تصویر، ویدیو، فایل و...) - پیامهایی که FileId دارند
+ ///
+ Task> GetMediaMessagesAsync(Guid taskId, int pageNumber, int pageSize);
+
+ ///
+ /// اضافه کردن پیام جدید
+ ///
+ Task AddAsync(TaskChatMessage message);
+
+ ///
+ /// بهروزرسانی پیام
+ ///
+ Task UpdateAsync(TaskChatMessage message);
+
+ ///
+ /// حذف فیزیکی پیام (در صورت نیاز - معمولاً استفاده نمیشود)
+ ///
+ Task DeleteAsync(TaskChatMessage message);
+
+ ///
+ /// ذخیره تغییرات
+ ///
+ Task SaveChangesAsync();
+
+ ///
+ /// بررسی وجود پیام
+ ///
+ Task ExistsAsync(Guid messageId);
+}
+
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