diff --git a/Marco.Pms.Model/Dtos/Forum/AddCommentDto.cs b/Marco.Pms.Model/Dtos/Forum/AddCommentDto.cs index c0772c9..b7d9e9d 100644 --- a/Marco.Pms.Model/Dtos/Forum/AddCommentDto.cs +++ b/Marco.Pms.Model/Dtos/Forum/AddCommentDto.cs @@ -1,12 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Marco.Pms.Model.Dtos.Forum +namespace Marco.Pms.Model.Dtos.Forum { public class AddCommentDto { + public Guid Id { get; set; } = Guid.Empty; + public Guid TicketId { get; set; } = Guid.Empty; + public Guid AuthorId { get; set; } + public string MessageText { get; set; } = string.Empty; + public DateTime SentAt { get; set; } + public Guid? ParentMessageId { get; set; } // For threaded replies + public ICollection? Attachments { get; set; } + public int TenantId { get; set; } } } diff --git a/Marco.Pms.Model/Dtos/Forum/CreateTicketDto.cs b/Marco.Pms.Model/Dtos/Forum/CreateTicketDto.cs index 25967ec..174f384 100644 --- a/Marco.Pms.Model/Dtos/Forum/CreateTicketDto.cs +++ b/Marco.Pms.Model/Dtos/Forum/CreateTicketDto.cs @@ -2,5 +2,17 @@ { public class CreateTicketDto { + public Guid Id { get; set; } = Guid.Empty; + public string Subject { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public Guid StatusId { get; set; } + public Guid TypeId { get; set; } // QualityIssue, HelpDesk, Feedback + public int CreatedById { get; set; } + public DateTime CreatedAt { get; set; } + public int LinkedActivityId { get; set; } // task or project ID + public ICollection? Attachments { get; set; } + public Guid PriorityId { get; set; } + public ICollection? TagIds { get; set; } + public int TenantId { get; set; } } } diff --git a/Marco.Pms.Model/Dtos/Forum/ForumAttachmentDto.cs b/Marco.Pms.Model/Dtos/Forum/ForumAttachmentDto.cs index d392c4e..7d7a233 100644 --- a/Marco.Pms.Model/Dtos/Forum/ForumAttachmentDto.cs +++ b/Marco.Pms.Model/Dtos/Forum/ForumAttachmentDto.cs @@ -1,12 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Marco.Pms.Model.Dtos.Forum +namespace Marco.Pms.Model.Dtos.Forum { public class ForumAttachmentDto { + public Guid Id { get; set; } = Guid.Empty; + public Guid TicketId { get; set; } = Guid.Empty; + public Guid? CommentId { get; set; } + public string FileName { get; set; } = string.Empty; + public string? Base64Data { get; set; } + public int FileSize { get; set; } + public string ContentType { get; set; } = string.Empty; + public DateTime SentAt { get; set; } } } diff --git a/Marco.Pms.Model/Dtos/Forum/UpdateCommentDto.cs b/Marco.Pms.Model/Dtos/Forum/UpdateCommentDto.cs new file mode 100644 index 0000000..971c30c --- /dev/null +++ b/Marco.Pms.Model/Dtos/Forum/UpdateCommentDto.cs @@ -0,0 +1,14 @@ +namespace Marco.Pms.Model.Dtos.Forum +{ + public class UpdateCommentDto + { + public Guid Id { get; set; } + public Guid TicketId { get; set; } + public Guid AuthorId { get; set; } + public string MessageText { get; set; } = string.Empty; + public DateTime SentAt { get; set; } + public Guid? ParentMessageId { get; set; } // For threaded replies + public ICollection? Attachments { get; set; } + public int TenantId { get; set; } + } +} diff --git a/Marco.Pms.Model/Dtos/Forum/UpdateTicketDto.cs b/Marco.Pms.Model/Dtos/Forum/UpdateTicketDto.cs new file mode 100644 index 0000000..cc893c2 --- /dev/null +++ b/Marco.Pms.Model/Dtos/Forum/UpdateTicketDto.cs @@ -0,0 +1,18 @@ +namespace Marco.Pms.Model.Dtos.Forum +{ + public class UpdateTicketDto + { + public Guid Id { get; set; } + public string Subject { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public Guid StatusId { get; set; } + public Guid TypeId { get; set; } // QualityIssue, HelpDesk, Feedback + public int CreatedById { get; set; } + public DateTime CreatedAt { get; set; } + public int LinkedActivityId { get; set; } // task or project ID + public ICollection? Attachments { get; set; } + public Guid PriorityId { get; set; } + public ICollection? TagIds { get; set; } + public int TenantId { get; set; } + } +} diff --git a/Marco.Pms.Model/Forum/TicketAttachment.cs b/Marco.Pms.Model/Forum/TicketAttachment.cs index f1ed184..006519b 100644 --- a/Marco.Pms.Model/Forum/TicketAttachment.cs +++ b/Marco.Pms.Model/Forum/TicketAttachment.cs @@ -11,7 +11,7 @@ namespace Marco.Pms.Model.Forum [ValidateNever] [ForeignKey(nameof(TicketId))] public TicketForum? Ticket { get; set; } - public Guid CommentId { get; set; } + public Guid? CommentId { get; set; } [ValidateNever] [ForeignKey(nameof(CommentId))] public TicketComment? TicketComment { get; set; } diff --git a/Marco.Pms.Model/Forum/TicketTag.cs b/Marco.Pms.Model/Forum/TicketTag.cs index 5cd0700..98b6f4d 100644 --- a/Marco.Pms.Model/Forum/TicketTag.cs +++ b/Marco.Pms.Model/Forum/TicketTag.cs @@ -4,10 +4,10 @@ { public int Id { get; set; } public Guid TicketId { get; set; } - public TicketForum Ticket { get; set; } = new TicketForum(); + public TicketForum? Ticket { get; set; } public Guid TagId { get; set; } - public TicketTagMaster Tag { get; set; } = new TicketTagMaster(); + public TicketTagMaster? Tag { get; set; } } } diff --git a/Marco.Pms.Model/Mapper/ForumMapper.cs b/Marco.Pms.Model/Mapper/ForumMapper.cs new file mode 100644 index 0000000..5dbf8da --- /dev/null +++ b/Marco.Pms.Model/Mapper/ForumMapper.cs @@ -0,0 +1,176 @@ +using Marco.Pms.Model.DocumentManager; +using Marco.Pms.Model.Dtos.Forum; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Forum; +using Marco.Pms.Model.ViewModels.Forum; + +namespace Marco.Pms.Model.Mapper +{ + public static class ForumMapper + { + public static TicketForum ToTicketForumFromCreateTicketDto(this CreateTicketDto createTicketDto) + { + return new TicketForum + { + Subject = createTicketDto.Subject, + Description = createTicketDto.Description, + StatusId = createTicketDto.StatusId, + TypeId = createTicketDto.TypeId, + CreatedById = createTicketDto.CreatedById, + CreatedAt = createTicketDto.CreatedAt, + LinkedActivityId = createTicketDto.LinkedActivityId, + PriorityId = createTicketDto.PriorityId, + TenantId = createTicketDto.TenantId, + }; + } + public static TicketForum ToTicketForumFromUpdateTicketDto(this UpdateTicketDto updateTicketDto, TicketForum ticket) + { + return new TicketForum + { + Id = updateTicketDto.Id, + Subject = updateTicketDto.Subject, + Description = updateTicketDto.Description, + StatusId = updateTicketDto.StatusId, + TypeId = updateTicketDto.TypeId, + CreatedById = ticket.CreatedById, + CreatedAt = ticket.CreatedAt, + LinkedActivityId = updateTicketDto.LinkedActivityId, + PriorityId = updateTicketDto.PriorityId, + TenantId = ticket.TenantId + }; + } + + public static TicketComment ToTicketCommentFromAddCommentDto(this AddCommentDto commentDto) + { + return new TicketComment + { + TicketId = commentDto.TicketId, + AuthorId = commentDto.AuthorId, + MessageText = commentDto.MessageText, + SentAt = commentDto.SentAt, + ParentMessageId = commentDto.ParentMessageId, + TenantId = commentDto.TenantId, + }; + } + public static TicketComment ToTicketCommentFromUpdateCommentDto(this UpdateCommentDto updateComment, int tenantId, TicketComment comment) + { + return new TicketComment + { + Id = updateComment.Id, + TicketId = updateComment.TicketId, + AuthorId = comment.AuthorId, + MessageText = updateComment.MessageText, + SentAt = comment.SentAt, + ParentMessageId = updateComment.ParentMessageId, + TenantId = tenantId, + }; + } + public static TicketAttachment ToTicketAttachmentFromForumAttachmentDto(this ForumAttachmentDto AttachmentDto, Guid ticketId, Guid fileId, Guid? commentId = null) + { + return new TicketAttachment + { + TicketId = ticketId, + CommentId = commentId, + FileName = AttachmentDto.FileName, + FileId = fileId, + }; + } + + public static Document ToDocumentFromForumAttachmentDto(this ForumAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt, int tenantId) + { + return new Document + { + FileName = AttachmentDto.FileName, + ContentType = AttachmentDto.ContentType, + S3Key = objectKey, + ThumbS3Key = thumbS3Key, + Base64Data = AttachmentDto.Base64Data, + FileSize = AttachmentDto.FileSize, + UploadedAt = uploadedAt, + TenantId = tenantId + }; + } + public static ForumTicketVM ToForumTicketVMFromTicketForum(this TicketForum ticket, Employee employee) + { + return new ForumTicketVM + { + Id = ticket.Id, + Subject = ticket.Subject, + Description = ticket.Description, + CreatedAt = ticket.CreatedAt, + LinkedActivityId = ticket.LinkedActivityId, + Status = ticket.TicketStatusMaster.ToTicketStatusVMFromTicketStatusMaster(), + Priority = ticket.Priority.ToTicketPriorityVMFromTicketPriorityMaster(), + Type = ticket.TicketTypeMaster.ToTicketTypeVMFromTicketTypeMaster(), + CreatedBy = employee.ToBasicEmployeeVMFromEmployee(), + }; + } + public static TicketAttachmentVM ToTicketAttachmentVMFromTicketAttachment(this TicketAttachment attachment, string preSignedUrl, string thumbPreSignedUrl) + { + return new TicketAttachmentVM + { + Id = attachment.Id, + TicketId = attachment.TicketId, + CommentId = attachment.CommentId, + FileName = attachment.FileName, + PreSignedUrl = preSignedUrl, + ThumbPreSignedUrl = thumbPreSignedUrl + }; + } + public static TicketCommentVM ToTicketCommentVMFromTicketComment(this TicketComment comment, Employee employee) + { + return new TicketCommentVM + { + Id = comment.Id, + TicketId = comment.TicketId, + Author = employee.ToBasicEmployeeVMFromEmployee(), + MessageText = comment.MessageText, + SentAt = comment.SentAt, + ParentMessageId = comment.ParentMessageId, + Attachments = new List() + }; + } + public static TicketStatusVM ToTicketStatusVMFromTicketStatusMaster(this TicketStatusMaster statusMaster) + { + return new TicketStatusVM + { + Id = statusMaster.Id, + Name = statusMaster.Name, + Description = statusMaster.Description, + ColorCode = statusMaster.ColorCode, + IsDefault = statusMaster.IsDefault + }; + } + public static TicketPriorityVM ToTicketPriorityVMFromTicketPriorityMaster(this TicketPriorityMaster priorityMaster) + { + return new TicketPriorityVM + { + Id = priorityMaster.Id, + Name = priorityMaster.Name, + Level = priorityMaster.Level, + ColorCode = priorityMaster.ColorCode, + IsDefault = priorityMaster.IsDefault + }; + } + public static TicketTypeVM ToTicketTypeVMFromTicketTypeMaster(this TicketTypeMaster typeMaster) + { + return new TicketTypeVM + { + Id = typeMaster.Id, + Name = typeMaster.Name, + Description = typeMaster.Description, + IsDefault = typeMaster.IsDefault + }; + } + public static TicketTagVM ToTicketTagVMFromTicketTagMaster(this TicketTagMaster tagMaster) + { + return new TicketTagVM + { + Id = tagMaster.Id, + Name = tagMaster.Name, + ColorCode = tagMaster.ColorCode, + IsDefault = tagMaster.IsDefault + }; + } + } +} diff --git a/Marco.Pms.Model/ViewModels/DocumentManager/DocumentVM.cs b/Marco.Pms.Model/ViewModels/DocumentManager/DocumentVM.cs new file mode 100644 index 0000000..4940600 --- /dev/null +++ b/Marco.Pms.Model/ViewModels/DocumentManager/DocumentVM.cs @@ -0,0 +1,14 @@ +namespace Marco.Pms.Model.ViewModels.DocumentManager +{ + public class DocumentVM + { + public Guid Id { get; set; } + public Guid? BatchId { get; set; } + public string FileName { get; set; } = string.Empty; + public string? PreSignedUrl { get; set; } + public string? ThumbPreSignedUrl { get; set; } + public long FileSize { get; set; } + public string ContentType { get; set; } = string.Empty; + public DateTime UploadedAt { get; set; } + } +} diff --git a/Marco.Pms.Model/ViewModels/Forum/ForumTicketVM.cs b/Marco.Pms.Model/ViewModels/Forum/ForumTicketVM.cs index aa83753..9b236b9 100644 --- a/Marco.Pms.Model/ViewModels/Forum/ForumTicketVM.cs +++ b/Marco.Pms.Model/ViewModels/Forum/ForumTicketVM.cs @@ -1,6 +1,20 @@ -namespace Marco.Pms.Model.ViewModels.Forum +using Marco.Pms.Model.ViewModels.Activities; + +namespace Marco.Pms.Model.ViewModels.Forum { public class ForumTicketVM { + public Guid Id { get; set; } + public string Subject { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public TicketStatusVM? Status { get; set; } + public TicketTypeVM? Type { get; set; } // QualityIssue, HelpDesk, Feedback + public TicketPriorityVM? Priority { get; set; } + public BasicEmployeeVM? CreatedBy { get; set; } + public DateTime CreatedAt { get; set; } + public int LinkedActivityId { get; set; } // task or project ID + public ICollection? Comments { get; set; } // view model + public ICollection? Attachments { get; set; } // view model + public ICollection? Tags { get; set; } } } diff --git a/Marco.Pms.Model/ViewModels/Forum/TicketAttachmentVM.cs b/Marco.Pms.Model/ViewModels/Forum/TicketAttachmentVM.cs new file mode 100644 index 0000000..e82bd1f --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Forum/TicketAttachmentVM.cs @@ -0,0 +1,12 @@ +namespace Marco.Pms.Model.ViewModels.Forum +{ + public class TicketAttachmentVM + { + public Guid Id { get; set; } + public Guid TicketId { get; set; } + public Guid? CommentId { get; set; } + public string FileName { get; set; } = string.Empty; + public string? PreSignedUrl { get; set; } + public string ThumbPreSignedUrl { get; set; } = string.Empty; + } +} diff --git a/Marco.Pms.Model/ViewModels/Forum/TicketCommentVM.cs b/Marco.Pms.Model/ViewModels/Forum/TicketCommentVM.cs new file mode 100644 index 0000000..90f4e22 --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Forum/TicketCommentVM.cs @@ -0,0 +1,16 @@ +using Marco.Pms.Model.ViewModels.Activities; + +namespace Marco.Pms.Model.ViewModels.Forum +{ + public class TicketCommentVM + { + public Guid Id { get; set; } + public Guid TicketId { get; set; } + public BasicEmployeeVM? Author { get; set; } + public string MessageText { get; set; } = string.Empty; + public DateTime SentAt { get; set; } + public Guid? ParentMessageId { get; set; } // For threaded replies + + public ICollection? Attachments { get; set; } + } +} diff --git a/Marco.Pms.Model/ViewModels/Forum/TicketPriorityVM.cs b/Marco.Pms.Model/ViewModels/Forum/TicketPriorityVM.cs new file mode 100644 index 0000000..7986f62 --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Forum/TicketPriorityVM.cs @@ -0,0 +1,12 @@ +namespace Marco.Pms.Model.ViewModels.Forum +{ + public class TicketPriorityVM + { + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; // e.g., Low, Medium, High, Critical + public int Level { get; set; } // 1 = Low, 2 = Medium... + public string? ColorCode { get; set; } + public bool IsDefault { get; set; } // true for system defaults + + } +} diff --git a/Marco.Pms.Model/ViewModels/Forum/TicketStatusVM.cs b/Marco.Pms.Model/ViewModels/Forum/TicketStatusVM.cs new file mode 100644 index 0000000..79deff3 --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Forum/TicketStatusVM.cs @@ -0,0 +1,11 @@ +namespace Marco.Pms.Model.ViewModels.Forum +{ + public class TicketStatusVM + { + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; // e.g., "Open", "In Progress" + public string? Description { get; set; } + public string? ColorCode { get; set; } // e.g., "#FF0000" + public bool IsDefault { get; set; } // true for system defaults + } +} diff --git a/Marco.Pms.Model/ViewModels/Forum/TicketTagVM.cs b/Marco.Pms.Model/ViewModels/Forum/TicketTagVM.cs new file mode 100644 index 0000000..8be9f38 --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Forum/TicketTagVM.cs @@ -0,0 +1,10 @@ +namespace Marco.Pms.Model.ViewModels.Forum +{ + public class TicketTagVM + { + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; // e.g., "Bug", "UI", "Urgent" + public string? ColorCode { get; set; } + public bool IsDefault { get; set; } + } +} diff --git a/Marco.Pms.Model/ViewModels/Forum/TicketTypeVM.cs b/Marco.Pms.Model/ViewModels/Forum/TicketTypeVM.cs new file mode 100644 index 0000000..0339c1a --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Forum/TicketTypeVM.cs @@ -0,0 +1,11 @@ +namespace Marco.Pms.Model.ViewModels.Forum +{ + public class TicketTypeVM + { + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; // e.g., "Quality Issue" + public string? Description { get; set; } + public bool IsDefault { get; set; } // true for system defaults + + } +} diff --git a/Marco.Pms.Services/Controllers/ForumController.cs b/Marco.Pms.Services/Controllers/ForumController.cs index 8b45d4f..ce62d11 100644 --- a/Marco.Pms.Services/Controllers/ForumController.cs +++ b/Marco.Pms.Services/Controllers/ForumController.cs @@ -1,73 +1,796 @@ using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Authentication; +using Marco.Pms.Model.DocumentManager; using Marco.Pms.Model.Dtos.Forum; -using Marco.Pms.Model.Dtos.Util; -using Marco.Pms.Model.Industries; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Forum; +using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Forum; +using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace Marco.Pms.Services.Controllers { + [Authorize] [ApiController] [Route("api/[controller]")] public class ForumController : ControllerBase { - private readonly UserManager _userManager; private readonly ApplicationDbContext _context; - private readonly JwtSettings _jwtSettings; - private readonly RefreshTokenService _refreshTokenService; - private readonly IEmailSender _emailSender; - private readonly IConfiguration _configuration; - private readonly EmployeeHelper _employeeHelper; + private readonly UserHelper _userHelper; + private readonly S3UploadService _s3Service; + private readonly ILoggingService _logger; //string tenentId = "1"; - public ForumController(UserManager userManager, ApplicationDbContext context, JwtSettings jwtSettings, RefreshTokenService refreshTokenService, - IEmailSender emailSender, IConfiguration configuration, EmployeeHelper employeeHelper) + public ForumController(ApplicationDbContext context, S3UploadService s3Service, UserHelper userHelper, ILoggingService logger) { - _userManager = userManager; - _jwtSettings = jwtSettings; - _refreshTokenService = refreshTokenService; - _emailSender = emailSender; - _configuration = configuration; - _employeeHelper = employeeHelper; _context = context; + _userHelper = userHelper; + _s3Service = s3Service; + _logger = logger; } - [HttpPost("tickets")] + [HttpPost("ticket")] public async Task CreateNewTicket([FromBody] CreateTicketDto createTicketDto) - { - return NotFound(ApiResponse.ErrorResponse("Industry not found.", "Industry not found.", 404)); + { + if (!ModelState.IsValid) + { + var errors = ModelState.Values + .SelectMany(v => v.Errors) + .Select(e => e.ErrorMessage) + .ToList(); + _logger.LogError("{error}", errors); + return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); + } + int tenantId = _userHelper.GetTenantId(); + if (tenantId == createTicketDto.TenantId) + { + TicketForum ticketForum = createTicketDto.ToTicketForumFromCreateTicketDto(); + _context.Tickets.Add(ticketForum); + await _context.SaveChangesAsync(); + + List attachments = new List(); + List ticketTags = new List(); + List documents = new List(); + List tagVMs = new List(); + + if (createTicketDto.Attachments != null) + { + foreach (var attachmentDto in createTicketDto.Attachments) + { + byte[] fileBytes; + var Image = attachmentDto; + if (string.IsNullOrEmpty(Image.Base64Data)) + { + _logger.LogError("Base64 data is missing"); + return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); + } + try + { + //If base64 has a data URI prefix, strip it + var base64 = Image.Base64Data.Contains(",") + ? Image.Base64Data.Substring(Image.Base64Data.IndexOf(",") + 1) + : Image.Base64Data; + + fileBytes = Convert.FromBase64String(base64); + } + catch (Exception ex) + { + _logger.LogError("{error}", ex.Message); + return BadRequest(ApiResponse.ErrorResponse(ex.Message, ex, 400)); ; + } + + using var stream = new MemoryStream(fileBytes); + + + string fileName = _s3Service.GenerateFileName(Image.ContentType, tenantId, string.Empty); + + var objectKey = await _s3Service.UploadFileAsync(stream, fileName, Image.ContentType); + + Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, createTicketDto.CreatedAt, tenantId); + _context.Documents.Add(document); + await _context.SaveChangesAsync(); + + documents.Add(document); + + var attachment = attachmentDto.ToTicketAttachmentFromForumAttachmentDto(ticketForum.Id, document.Id); + attachments.Add(attachment); + } + _context.TicketAttachments.AddRange(attachments); + + } + if (createTicketDto.TagIds != null) + { + List? tagMasters = await _context.TicketTagMasters.Where(t => createTicketDto.TagIds.Contains(t.Id)).ToListAsync(); + foreach (var ticketTag in tagMasters) + { + TicketTagVM tagVM = ticketTag.ToTicketTagVMFromTicketTagMaster(); + + TicketTag tag = new TicketTag + { + TicketId = ticketForum.Id, + TagId = ticketTag.Id + }; + tagVMs.Add(tagVM); + ticketTags.Add(tag); + } + + _context.TicketTags.AddRange(ticketTags); + } + await _context.SaveChangesAsync(); + + var ticket = await _context.Tickets.Include(t => t.TicketTypeMaster).Include(t => t.TicketStatusMaster).Include(t => t.Priority).FirstOrDefaultAsync(t => t.Id == ticketForum.Id); + + Employee? employee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == ticketForum.CreatedById); + + + ForumTicketVM ticketVM = ticket.ToForumTicketVMFromTicketForum(employee); + + ticketVM.Tags = tagVMs; + + List attachmentVMs = new List(); + foreach (var attachment in attachments) + { + var document = documents.Find(d => d.Id == attachment.FileId); + string preSignedUrl = string.Empty; + if (document != null) + { + preSignedUrl = await _s3Service.GeneratePreSignedUrlAsync(document.S3Key); + } + attachmentVMs.Add(attachment.ToTicketAttachmentVMFromTicketAttachment(preSignedUrl, preSignedUrl)); + } + ticketVM.Attachments = attachmentVMs; + _logger.LogInfo("Ticket created by Employee {EmployeeId}", ticketForum.CreatedById); + return Ok(ApiResponse.SuccessResponse(ticketVM, "Ticket Created Successfully", 200)); + } + _logger.LogWarning("Employee {EmployeeId} tries to create ticket in different Tenant", createTicketDto.CreatedById); + return Unauthorized(ApiResponse.ErrorResponse("Not Authorized", "Not Authorized", 401)); } - - [HttpPost("tickets/{id}/comment")] + [HttpPost("ticket/edit")] + public async Task UpdateNewTicket([FromBody] UpdateTicketDto updateTicketDto) + { + if (!ModelState.IsValid) + { + var errors = ModelState.Values + .SelectMany(v => v.Errors) + .Select(e => e.ErrorMessage) + .ToList(); + _logger.LogError("{error}", errors); + return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); + } + int tenantId = _userHelper.GetTenantId(); + if (tenantId == updateTicketDto.TenantId) + { + var existingTicket = await _context.Tickets.Include(t => t.TicketTypeMaster).Include(t => t.TicketStatusMaster).Include(t => t.Priority).AsNoTracking().FirstOrDefaultAsync(t => t.Id == updateTicketDto.Id); + if (existingTicket != null) + { + TicketForum ticketForum = updateTicketDto.ToTicketForumFromUpdateTicketDto(existingTicket); + _context.Tickets.Update(ticketForum); + await _context.SaveChangesAsync(); + + + List attachments = new List(); + List ticketTags = new List(); + List tagVMs = new List(); + + List existingAttachments = await _context.TicketAttachments.Where(a => a.TicketId == updateTicketDto.Id).ToListAsync(); + var existingattachmentids = existingAttachments.Select(a => a.Id).ToList(); + var attachmentDtoids = updateTicketDto.Attachments.Select(a => a.Id).ToList(); + foreach (var attachmentDto in updateTicketDto.Attachments) + { + if (!existingattachmentids.Contains(attachmentDto.Id) && attachmentDto.TicketId != updateTicketDto.Id) + { + byte[] fileBytes; + var Image = attachmentDto; + if (string.IsNullOrEmpty(Image.Base64Data)) + { + _logger.LogError("Base64 data is missing"); + return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); + } + try + { + //If base64 has a data URI prefix, strip it + var base64 = Image.Base64Data.Contains(",") + ? Image.Base64Data.Substring(Image.Base64Data.IndexOf(",") + 1) + : Image.Base64Data; + + fileBytes = Convert.FromBase64String(base64); + } + catch (Exception ex) + { + _logger.LogError("{error}", ex.Message); + return BadRequest(ApiResponse.ErrorResponse(ex.Message, ex, 400)); ; + } + + using var stream = new MemoryStream(fileBytes); + + + string fileName = _s3Service.GenerateFileName(Image.ContentType, tenantId, string.Empty); + + var objectKey = await _s3Service.UploadFileAsync(stream, fileName, Image.ContentType); + + Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, updateTicketDto.CreatedAt, tenantId); + _context.Documents.Add(document); + await _context.SaveChangesAsync(); + + var attachment = attachmentDto.ToTicketAttachmentFromForumAttachmentDto(ticketForum.Id, document.Id); + attachments.Add(attachment); + } + } + if (attachments.Count != 0) + { + _context.TicketAttachments.AddRange(attachments); + } + var deletedAttachments = existingAttachments.Where(a => !attachmentDtoids.Contains(a.Id) && a.CommentId == null).ToList(); + var deletedFileIds = deletedAttachments.Select(a => a.FileId).ToList(); + if (deletedFileIds.Count != 0) + { + List existingDocuments = await _context.Documents.Where(d => deletedFileIds.Contains(d.Id)).ToListAsync(); + foreach (var existingDocument in existingDocuments) + { + await _s3Service.DeleteFileAsync(existingDocument.S3Key); + //await _s3Service.DeleteFileAsync(existingDocument.ThumbS3Key); + } + _context.TicketAttachments.RemoveRange(deletedAttachments); + } + + List existingTicketTags = await _context.TicketTags.Where(t => t.TicketId == updateTicketDto.Id).ToListAsync(); + List? tagMasters = await _context.TicketTagMasters.Where(t => updateTicketDto.TagIds.Contains(t.Id)).ToListAsync(); + var existingTicketTagIds = existingTicketTags.Select(t => t.TagId).ToList(); + + foreach (var ticketTag in tagMasters) + { + TicketTagVM tagVM = ticketTag.ToTicketTagVMFromTicketTagMaster(); + + TicketTag tag = new TicketTag + { + TicketId = ticketForum.Id, + TagId = ticketTag.Id + }; + tagVMs.Add(tagVM); + var demo = !existingTicketTagIds.Contains(tag.TagId); + if (!existingTicketTagIds.Contains(tag.TagId)) + { + ticketTags.Add(tag); + } + } + + if (ticketTags != null) + { + _context.TicketTags.AddRange(ticketTags); + } + var deletedTicketTags = existingTicketTags.Where(t => !updateTicketDto.TagIds.Contains(t.TagId)).ToList(); + if (deletedTicketTags.Count != 0) + { + _context.TicketTags.RemoveRange(deletedTicketTags); + } + await _context.SaveChangesAsync(); + + var ticket = await _context.Tickets.Include(t => t.TicketTypeMaster).Include(t => t.TicketStatusMaster).Include(t => t.Priority).FirstOrDefaultAsync(t => t.Id == ticketForum.Id); + + List comments = await _context.TicketComments.Where(c => c.TicketId == ticket.Id).ToListAsync(); + var authorIds = comments.Select(c => c.AuthorId.ToString()).ToList(); + + List? employees = await _context.Employees.Where(e => e.Id == ticketForum.CreatedById || authorIds.Contains(e.ApplicationUserId ?? "")).ToListAsync(); + + Employee employee = employees.Find(e => e.Id == ticketForum.CreatedById); + ForumTicketVM ticketVM = ticket.ToForumTicketVMFromTicketForum(employee); + + ticketVM.Tags = tagVMs; + + List commentVMs = new List(); + foreach (var comment in comments) + { + employee = employees.Find(e => e.ApplicationUserId == comment.AuthorId.ToString()) ?? new Employee(); + commentVMs.Add(comment.ToTicketCommentVMFromTicketComment(employee)); + } + + attachments = await _context.TicketAttachments.Where(a => a.TicketId == updateTicketDto.Id).ToListAsync(); + var fileIds = attachments.Select(a => a.FileId).ToList(); + List documents = await _context.Documents.Where(d => fileIds.Contains(d.Id)).ToListAsync(); + List ticketAttachmentVMs = new List(); + foreach (var attachment in attachments) + { + var document = documents.Find(d => d.Id == attachment.FileId); + string preSignedUrl = string.Empty; + if (document != null) + { + preSignedUrl = await _s3Service.GeneratePreSignedUrlAsync(document.S3Key); + } + if (attachment.CommentId == null) + { + ticketAttachmentVMs.Add(attachment.ToTicketAttachmentVMFromTicketAttachment(preSignedUrl, preSignedUrl)); + } + else + { + var commentVM = commentVMs.Find(c => c.Id == attachment.CommentId); + commentVM.Attachments.Add(attachment.ToTicketAttachmentVMFromTicketAttachment(preSignedUrl, preSignedUrl)); + } + } + ticketVM.Comments = commentVMs; + ticketVM.Attachments = ticketAttachmentVMs; + _logger.LogInfo("Ticket {TicketId} updated", updateTicketDto.Id); + return Ok(ApiResponse.SuccessResponse(ticketVM, "Ticket Updated Successfully", 200)); + } + _logger.LogError("Ticket {TicketId} not Found in database", updateTicketDto.Id); + return NotFound(ApiResponse.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); + } + _logger.LogWarning("Employee {EmployeeId} tries to create ticket in different Tenant", updateTicketDto.CreatedById); + return Unauthorized(ApiResponse.ErrorResponse("Not Authorized", "Not Authorized", 401)); + } + + [HttpPost("ticket/comment")] public async Task AddComment([FromBody] AddCommentDto addCommentDto) - { - return NotFound(ApiResponse.ErrorResponse("Industry not found.", "Industry not found.", 404)); + { + if (!ModelState.IsValid) + { + var errors = ModelState.Values + .SelectMany(v => v.Errors) + .Select(e => e.ErrorMessage) + .ToList(); + _logger.LogError("{error}", errors); + return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); + } + + int tenantId = _userHelper.GetTenantId(); + List attachments = new List(); + List documents = new List(); + + TicketComment comment = addCommentDto.ToTicketCommentFromAddCommentDto(); + _context.TicketComments.Add(comment); + await _context.SaveChangesAsync(); + + if (addCommentDto.Attachments != null) + { + foreach (var attachmentDto in addCommentDto.Attachments) + { + byte[] fileBytes; + var Image = attachmentDto; + if (string.IsNullOrEmpty(Image.Base64Data)) + { + _logger.LogError("Base64 data is missing"); + return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); + } + try + { + //If base64 has a data URI prefix, strip it + var base64 = Image.Base64Data.Contains(",") + ? Image.Base64Data.Substring(Image.Base64Data.IndexOf(",") + 1) + : Image.Base64Data; + + fileBytes = Convert.FromBase64String(base64); + } + catch (Exception ex) + { + _logger.LogError("{error}", ex.Message); + return BadRequest(ApiResponse.ErrorResponse(ex.Message, ex, 400)); ; + } + using var stream = new MemoryStream(fileBytes); + + + string fileName = _s3Service.GenerateFileName(Image.ContentType, tenantId, string.Empty); + + var objectKey = await _s3Service.UploadFileAsync(stream, fileName, Image.ContentType); + + Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, addCommentDto.SentAt, tenantId); + _context.Documents.Add(document); + await _context.SaveChangesAsync(); + + documents.Add(document); + + var attachment = attachmentDto.ToTicketAttachmentFromForumAttachmentDto(addCommentDto.TicketId, document.Id, comment.Id); + attachments.Add(attachment); + } + _context.TicketAttachments.AddRange(attachments); + await _context.SaveChangesAsync(); + } + Employee employee = await _context.Employees.FirstOrDefaultAsync(e => e.ApplicationUserId == addCommentDto.AuthorId.ToString()) ?? new Employee(); + TicketCommentVM commentVM = comment.ToTicketCommentVMFromTicketComment(employee); + + List attachmentVMs = new List(); + if (attachments != null) + { + foreach (var attachment in attachments) + { + var document = documents.Find(d => d.Id == attachment.FileId); + string preSignedUrl = string.Empty; + if (document != null) + { + preSignedUrl = await _s3Service.GeneratePreSignedUrlAsync(document.S3Key); + } + attachmentVMs.Add(attachment.ToTicketAttachmentVMFromTicketAttachment(preSignedUrl, preSignedUrl)); + } + } + commentVM.Attachments = attachmentVMs; + + _logger.LogInfo("User {ApplicationUserId} commented on a ticket", comment.AuthorId); + return Ok(ApiResponse.SuccessResponse(commentVM, "Comment Created Successfully", 200)); + } + [HttpPost("ticket/comment/edit")] + public async Task EditComment([FromBody] UpdateCommentDto updateCommentDto) + { + if (!ModelState.IsValid) + { + var errors = ModelState.Values + .SelectMany(v => v.Errors) + .Select(e => e.ErrorMessage) + .ToList(); + _logger.LogError("{error}", errors); + return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); + } + + int tenantId = _userHelper.GetTenantId(); + List attachments = new List(); + + TicketComment existingComment = await _context.TicketComments.AsNoTracking().FirstOrDefaultAsync(c => c.Id == updateCommentDto.Id) ?? new TicketComment(); + TicketComment updateComment = updateCommentDto.ToTicketCommentFromUpdateCommentDto(tenantId, existingComment); + _context.TicketComments.Update(updateComment); + await _context.SaveChangesAsync(); + + List existingAttachments = await _context.TicketAttachments.Where(a => a.CommentId == updateComment.Id).ToListAsync(); + var existingattachmentids = existingAttachments.Select(a => a.Id).ToList(); + var attachmentDtoids = updateCommentDto.Attachments.Select(a => a.Id).ToList(); + + foreach (var attachmentDto in updateCommentDto.Attachments) + { + if (!existingattachmentids.Contains(attachmentDto.Id) && attachmentDto.CommentId != updateComment.Id) + { + byte[] fileBytes; + var Image = attachmentDto; + if (string.IsNullOrEmpty(Image.Base64Data)) + { + _logger.LogError("Base64 data is missing"); + return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); + } + try + { + //If base64 has a data URI prefix, strip it + var base64 = Image.Base64Data.Contains(",") + ? Image.Base64Data.Substring(Image.Base64Data.IndexOf(",") + 1) + : Image.Base64Data; + + fileBytes = Convert.FromBase64String(base64); + } + catch (Exception ex) + { + _logger.LogError("{error}", ex.Message); + return BadRequest(ApiResponse.ErrorResponse(ex.Message, ex, 400)); ; + } + using var stream = new MemoryStream(fileBytes); + + + string fileName = _s3Service.GenerateFileName(Image.ContentType, tenantId, string.Empty); + + var objectKey = await _s3Service.UploadFileAsync(stream, fileName, Image.ContentType); + + Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, existingComment.SentAt, tenantId); + _context.Documents.Add(document); + await _context.SaveChangesAsync(); + + var attachment = attachmentDto.ToTicketAttachmentFromForumAttachmentDto(existingComment.TicketId, document.Id, updateComment.Id); + attachments.Add(attachment); + } + } + if (attachments.Count != 0) + { + _context.TicketAttachments.AddRange(attachments); + } + var deletedAttachments = existingAttachments.Where(a => !attachmentDtoids.Contains(a.Id) && a.CommentId != null).ToList(); + var deletedFileIds = deletedAttachments.Select(a => a.FileId).ToList(); + if (deletedFileIds.Count != 0) + { + List existingDocuments = await _context.Documents.Where(d => deletedFileIds.Contains(d.Id)).ToListAsync(); + foreach (var existingDocument in existingDocuments) + { + await _s3Service.DeleteFileAsync(existingDocument.S3Key); + //await _s3Service.DeleteFileAsync(existingDocument.ThumbS3Key); + } + _context.TicketAttachments.RemoveRange(deletedAttachments); + } + await _context.SaveChangesAsync(); + + Employee employee = await _context.Employees.FirstOrDefaultAsync(e => e.ApplicationUserId == existingComment.AuthorId.ToString()) ?? new Employee(); + TicketCommentVM commentVM = updateComment.ToTicketCommentVMFromTicketComment(employee); + + List attachmentVMs = new List(); + attachments = await _context.TicketAttachments.Where(a => a.CommentId == updateCommentDto.Id).ToListAsync(); + var fileIds = attachments.Select(a => a.FileId).ToList(); + List documents = await _context.Documents.Where(d => fileIds.Contains(d.Id)).ToListAsync(); + + if (attachments != null) + { + foreach (var attachment in attachments) + { + var document = documents.Find(d => d.Id == attachment.FileId); + string preSignedUrl = string.Empty; + if (document != null) + { + preSignedUrl = await _s3Service.GeneratePreSignedUrlAsync(document.S3Key); + } + attachmentVMs.Add(attachment.ToTicketAttachmentVMFromTicketAttachment(preSignedUrl, preSignedUrl)); + } + } + commentVM.Attachments = attachmentVMs; + + _logger.LogInfo("comment {CommentId} was Updated", updateComment.Id); + return Ok(ApiResponse.SuccessResponse(commentVM, "Comment Updated Successfully", 200)); } - [HttpPost("tickets/{id}/attachments")] - public async Task UploadAttachments([FromBody] ForumAttachmentDto forumAttachmentDto) + [HttpPost("ticket/attachment")] + public async Task UploadAttachments([FromBody] List forumAttachmentDtos) { - return NotFound(ApiResponse.ErrorResponse("Industry not found.", "Industry not found.", 404)); + if (!ModelState.IsValid) + { + var errors = ModelState.Values + .SelectMany(v => v.Errors) + .Select(e => e.ErrorMessage) + .ToList(); + _logger.LogError("{error}", errors); + return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); + } + + int tenantId = _userHelper.GetTenantId(); + List ticketAttachmentVMs = new List(); + + foreach (var forumAttachmentDto in forumAttachmentDtos) + { + byte[] fileBytes; + if (string.IsNullOrEmpty(forumAttachmentDto.Base64Data)) + { + _logger.LogError("Base64 data is missing"); + return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); + } + try + { + //If base64 has a data URI prefix, strip it + var base64 = forumAttachmentDto.Base64Data.Contains(",") + ? forumAttachmentDto.Base64Data.Substring(forumAttachmentDto.Base64Data.IndexOf(",") + 1) + : forumAttachmentDto.Base64Data; + + fileBytes = Convert.FromBase64String(base64); + } + catch (Exception ex) + { + _logger.LogError("{error}", ex.Message); + return BadRequest(ApiResponse.ErrorResponse(ex.Message, ex, 400)); ; + } + using var stream = new MemoryStream(fileBytes); + + + string fileName = _s3Service.GenerateFileName(forumAttachmentDto.ContentType, tenantId, string.Empty); + + var objectKey = await _s3Service.UploadFileAsync(stream, fileName, forumAttachmentDto.ContentType); + + Document document = forumAttachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, forumAttachmentDto.SentAt, tenantId); + _context.Documents.Add(document); + await _context.SaveChangesAsync(); + + var attachment = forumAttachmentDto.ToTicketAttachmentFromForumAttachmentDto(forumAttachmentDto.TicketId, document.Id, forumAttachmentDto.CommentId); + _context.TicketAttachments.Add(attachment); + await _context.SaveChangesAsync(); + + string preSignedUrl = await _s3Service.GeneratePreSignedUrlAsync(document.S3Key); + + TicketAttachmentVM attachmentVM = attachment.ToTicketAttachmentVMFromTicketAttachment(preSignedUrl, preSignedUrl); + ticketAttachmentVMs.Add(attachmentVM); + } + + _logger.LogInfo("Attachments were added"); + return Ok(ApiResponse.SuccessResponse(ticketAttachmentVMs, "attechments added Successfully", 200)); } - [HttpPatch("tickets/{id}/status")] - public async Task UpdateTicketStatus([FromBody] InquiryDto inquiryDto) + [HttpPatch("ticket/status/{id}")] + public async Task UpdateTicketStatus(Guid id, [FromQuery] Guid statusId) { - return NotFound(ApiResponse.ErrorResponse("Industry not found.", "Industry not found.", 404)); + int tenantId = _userHelper.GetTenantId(); + TicketForum ticket = await _context.Tickets.FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId) ?? new TicketForum(); + ticket.StatusId = statusId; + await _context.SaveChangesAsync(); + + ticket = await _context.Tickets.Include(t => t.TicketTypeMaster).Include(t => t.TicketStatusMaster).Include(t => t.Priority).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId) ?? new TicketForum(); + List comments = await _context.TicketComments.Where(c => c.TicketId == ticket.Id).ToListAsync(); + var authorIds = comments.Select(c => c.AuthorId.ToString()).ToList(); + + List employees = await _context.Employees.Where(e => e.Id == ticket.CreatedById || authorIds.Contains(e.ApplicationUserId ?? "")).ToListAsync(); + Employee employee = employees.Find(e => e.Id == ticket.CreatedById) ?? new Employee(); + + ForumTicketVM ticketVM = ticket.ToForumTicketVMFromTicketForum(employee); + + var ticketTags = await _context.TicketTags.Where(t => t.TicketId == ticket.Id).ToListAsync(); + List tagIds = ticketTags.Select(t => t.TagId).ToList(); + + List? tagMasters = await _context.TicketTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync(); + List tagVMs = new List(); + foreach (var ticketTag in tagMasters) + { + TicketTagVM tagVM = ticketTag.ToTicketTagVMFromTicketTagMaster(); + tagVMs.Add(tagVM); + } + ticketVM.Tags = tagVMs; + + List commentVMs = new List(); + foreach (var comment in comments) + { + employee = employees.Find(e => e.ApplicationUserId == comment.AuthorId.ToString()) ?? new Employee(); + commentVMs.Add(comment.ToTicketCommentVMFromTicketComment(employee)); + } + + List attachments = await _context.TicketAttachments.Where(a => a.TicketId == id).ToListAsync(); + List fileIds = attachments.Select(a => a.FileId).ToList(); + + List documents = await _context.Documents.Where(d => fileIds.Contains(d.Id)).ToListAsync(); + + List ticketAttachmentVMs = new List(); + foreach (var attachment in attachments) + { + var document = documents.Find(d => d.Id == attachment.FileId); + string preSignedUrl = string.Empty; + if (document != null) + { + preSignedUrl = await _s3Service.GeneratePreSignedUrlAsync(document.S3Key); + } + if (attachment.CommentId == null) + { + ticketAttachmentVMs.Add(attachment.ToTicketAttachmentVMFromTicketAttachment(preSignedUrl, preSignedUrl)); + } + else + { + var commentVM = commentVMs.Find(c => c.Id == attachment.CommentId); + commentVM.Attachments.Add(attachment.ToTicketAttachmentVMFromTicketAttachment(preSignedUrl, preSignedUrl)); + } + } + ticketVM.Comments = commentVMs; + ticketVM.Attachments = ticketAttachmentVMs; + + _logger.LogInfo("Status of Ticket {TicketId} is changes to {status}", id, ticket.TicketStatusMaster.Name); + return Ok(ApiResponse.SuccessResponse(ticketVM, "Ticket Fetched Successfully", 200)); } - [HttpGet("tickets/{id}")] + [HttpGet("ticket/{id}")] public async Task GetTicketDetail(Guid id) { - return NotFound(ApiResponse.ErrorResponse("Industry not found.", "Industry not found.", 404)); + int tenantId = _userHelper.GetTenantId(); + TicketForum ticket = await _context.Tickets.Include(t => t.TicketTypeMaster).Include(t => t.TicketStatusMaster).Include(t => t.Priority).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId) ?? new TicketForum(); + List comments = await _context.TicketComments.Where(c => c.TicketId == ticket.Id).ToListAsync(); + var authorIds = comments.Select(c => c.AuthorId.ToString()).ToList(); + + List employees = await _context.Employees.Where(e => e.Id == ticket.CreatedById || authorIds.Contains(e.ApplicationUserId ?? "")).ToListAsync(); + Employee employee = employees.Find(e => e.Id == ticket.CreatedById) ?? new Employee(); + + ForumTicketVM ticketVM = ticket.ToForumTicketVMFromTicketForum(employee); + + var ticketTags = await _context.TicketTags.Where(t => t.TicketId == ticket.Id).ToListAsync(); + List tagIds = ticketTags.Select(t => t.TagId).ToList(); + + List? tagMasters = await _context.TicketTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync(); + List tagVMs = new List(); + foreach (var ticketTag in tagMasters) + { + TicketTagVM tagVM = ticketTag.ToTicketTagVMFromTicketTagMaster(); + tagVMs.Add(tagVM); + } + ticketVM.Tags = tagVMs; + + List commentVMs = new List(); + foreach (var comment in comments) + { + employee = employees.Find(e => e.ApplicationUserId == comment.AuthorId.ToString()) ?? new Employee(); + commentVMs.Add(comment.ToTicketCommentVMFromTicketComment(employee)); + } + + List attachments = await _context.TicketAttachments.Where(a => a.TicketId == id).ToListAsync(); + List fileIds = attachments.Select(a => a.FileId).ToList(); + + List documents = await _context.Documents.Where(d => fileIds.Contains(d.Id)).ToListAsync(); + + List ticketAttachmentVMs = new List(); + foreach (var attachment in attachments) + { + var document = documents.Find(d => d.Id == attachment.FileId); + string preSignedUrl = string.Empty; + if (document != null) + { + preSignedUrl = await _s3Service.GeneratePreSignedUrlAsync(document.S3Key); + } + if (attachment.CommentId == null) + { + ticketAttachmentVMs.Add(attachment.ToTicketAttachmentVMFromTicketAttachment(preSignedUrl, preSignedUrl)); + } + else + { + var commentVM = commentVMs.Find(c => c.Id == attachment.CommentId); + commentVM.Attachments.Add(attachment.ToTicketAttachmentVMFromTicketAttachment(preSignedUrl, preSignedUrl)); + } + } + ticketVM.Comments = commentVMs; + ticketVM.Attachments = ticketAttachmentVMs; + + _logger.LogInfo("Fetched Ticket {TicketId}", id); + return Ok(ApiResponse.SuccessResponse(ticketVM, "Ticket Fetched Successfully", 200)); } [HttpGet("tickets")] - public async Task ListTickets([FromBody] InquiryDto inquiryDto) + public async Task ListTickets() { - return NotFound(ApiResponse.ErrorResponse("Industry not found.", "Industry not found.", 404)); + int tenantId = _userHelper.GetTenantId(); + List tickets = await _context.Tickets.Include(t => t.TicketTypeMaster).Include(t => t.TicketStatusMaster).Include(t => t.Priority).Where(t => t.TenantId == tenantId).ToListAsync(); + var createdByIds = tickets.Select(t => t.CreatedById).ToList(); + var ticketIds = tickets.Select(t => t.Id).ToList(); + + List ticketComments = await _context.TicketComments.Where(c => ticketIds.Contains(c.TicketId)).ToListAsync(); + var authorIds = ticketComments.Select(c => c.AuthorId.ToString()).ToList(); + + List employees = await _context.Employees.Where(e => createdByIds.Contains(e.Id) || authorIds.Contains(e.ApplicationUserId)).ToListAsync(); + + var ticketTags = await _context.TicketTags.Where(t => ticketIds.Contains(t.TicketId)).ToListAsync(); + List tagIds = ticketTags.Select(t => t.TagId).ToList(); + + List? tagMasters = await _context.TicketTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync(); + + List ticketAttachments = await _context.TicketAttachments.Where(a => ticketIds.Contains(a.TicketId)).ToListAsync(); + List documentIds = ticketAttachments.Select(a => a.FileId).ToList(); + + List documents = await _context.Documents.Where(d => documentIds.Contains(d.Id)).ToListAsync(); + + List ticketVMs = new List(); + foreach (var ticket in tickets) + { + Employee employee = employees.Find(e => e.Id == ticket.CreatedById) ?? new Employee(); + ForumTicketVM ticketVM = ticket.ToForumTicketVMFromTicketForum(employee); + + var ticketTagMappers = ticketTags.Where(t => t.TicketId == ticket.Id).ToList(); + List ticketTagIds = ticketTagMappers.Select(t => t.TagId).ToList(); + + List ticketTagMasters = tagMasters.Where(t => ticketTagIds.Contains(t.Id)).ToList(); + + List tagVMs = new List(); + foreach (var ticketTag in ticketTagMasters) + { + TicketTagVM tagVM = ticketTag.ToTicketTagVMFromTicketTagMaster(); + tagVMs.Add(tagVM); + } + + List comments = ticketComments.Where(c => c.TicketId == ticket.Id).ToList(); + List commentVMs = new List(); + foreach (var comment in comments) + { + employee = employees.Find(e => e.ApplicationUserId == comment.AuthorId.ToString()) ?? new Employee(); + commentVMs.Add(comment.ToTicketCommentVMFromTicketComment(employee)); + } + + List attachments = ticketAttachments.Where(a => a.TicketId == ticket.Id).ToList(); + List fileIds = attachments.Select(a => a.FileId).ToList(); + + List files = documents.Where(d => fileIds.Contains(d.Id)).ToList(); + + List ticketAttachmentVMs = new List(); + foreach (var attachment in attachments) + { + var document = documents.Find(d => d.Id == attachment.FileId); + string preSignedUrl = string.Empty; + if (document != null) + { + preSignedUrl = await _s3Service.GeneratePreSignedUrlAsync(document.S3Key); + } + if (attachment.CommentId == null) + { + ticketAttachmentVMs.Add(attachment.ToTicketAttachmentVMFromTicketAttachment(preSignedUrl, preSignedUrl)); + } + else + { + var commentVM = commentVMs.Find(c => c.Id == attachment.CommentId); + commentVM.Attachments.Add(attachment.ToTicketAttachmentVMFromTicketAttachment(preSignedUrl, preSignedUrl)); + } + } + ticketVM.Comments = commentVMs; + ticketVM.Attachments = ticketAttachmentVMs; + ticketVM.Tags = tagVMs; + ticketVMs.Add(ticketVM); + } + _logger.LogInfo("{count} tickets records fetched successfully from tenant {tenantId}", ticketVMs.Count, tenantId); + return Ok(ApiResponse.SuccessResponse(ticketVMs, System.String.Format("{0} tickets records fetched successfully", ticketVMs.Count), 200)); } } } diff --git a/Marco.Pms.Services/Controllers/MasterController.cs b/Marco.Pms.Services/Controllers/MasterController.cs index 5c24c20..129b57a 100644 --- a/Marco.Pms.Services/Controllers/MasterController.cs +++ b/Marco.Pms.Services/Controllers/MasterController.cs @@ -1,10 +1,13 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Activities; using Marco.Pms.Model.Entitlements; +using Marco.Pms.Model.Forum; using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; +using Marco.Pms.Model.ViewModels.Forum; using MarcoBMS.Services.Helpers; +using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -18,10 +21,12 @@ namespace Marco.Pms.Services.Controllers { private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; - public MasterController(ApplicationDbContext context, UserHelper userHelper) + private readonly ILoggingService _logger; + public MasterController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger) { _context = context; _userHelper = userHelper; + _logger = logger; } [HttpGet] [Route("activities")] @@ -30,14 +35,15 @@ namespace Marco.Pms.Services.Controllers var tenantId = _userHelper.GetTenantId(); var activities = await _context.ActivityMasters.Where(c => c.TenantId == tenantId && c.IsActive == true).ToListAsync(); List activitiesVM = new List(); - foreach (var activity in activities) { + foreach (var activity in activities) + { var checkList = await _context.ActivityCheckLists.Where(c => c.TenantId == tenantId && c.ActivityId == activity.Id).ToListAsync(); List checkListVM = new List(); - if(checkList != null) + if (checkList != null) { foreach (ActivityCheckList check in checkList) { - var checkVM = check.ToCheckListVMFromActivityCheckList(activity.Id,false); + var checkVM = check.ToCheckListVMFromActivityCheckList(activity.Id, false); checkListVM.Add(checkVM); } } @@ -45,7 +51,8 @@ namespace Marco.Pms.Services.Controllers ActivityVM activityVM = activity.ToActivityVMFromActivityMaster(checkListVM); activitiesVM.Add(activityVM); } - return Ok(ApiResponse.SuccessResponse(activitiesVM, "Success.", 200)); + _logger.LogInfo("{count} activity records fetched successfully from tenant {tenantId}", activitiesVM.Count, tenantId); + return Ok(ApiResponse.SuccessResponse(activitiesVM, System.String.Format("{0} activity records fetched successfully", activitiesVM.Count), 200)); } [HttpPost("activity")] @@ -55,81 +62,87 @@ namespace Marco.Pms.Services.Controllers var employee = await _userHelper.GetCurrentEmployeeAsync(); if (employee.TenantId != tenantId) { + _logger.LogWarning("User from tenant {employeeTenantId} tries to access data from tenant {tenantId}", employee.TenantId, tenantId); return Unauthorized(ApiResponse.ErrorResponse("Current tenant did not match with user's tenant", "Current tenant did not match with user's tenant", 401)); } var activityMaster = createActivity.ToActivityMasterFromCreateActivityMasterDto(tenantId); _context.ActivityMasters.Add(activityMaster); await _context.SaveChangesAsync(); + List checkListVM = new List(); - if (createActivity.CheckList != null) { + if (createActivity.CheckList != null) + { List activityCheckList = new List(); foreach (var check in createActivity.CheckList) { - ActivityCheckList checkList = check.ToActivityCheckListFromCreateCheckListDto(tenantId,activityMaster.Id); + ActivityCheckList checkList = check.ToActivityCheckListFromCreateCheckListDto(tenantId, activityMaster.Id); activityCheckList.Add(checkList); } _context.ActivityCheckLists.AddRange(activityCheckList); await _context.SaveChangesAsync(); - List checkListVM = new List(); + foreach (ActivityCheckList check in activityCheckList) { - var checkVM = check.ToCheckListVMFromActivityCheckList(activityMaster.Id,false); + var checkVM = check.ToCheckListVMFromActivityCheckList(activityMaster.Id, false); checkListVM.Add(checkVM); } - - ActivityVM activityVM = activityMaster.ToActivityVMFromActivityMaster(checkListVM); - return Ok(ApiResponse.SuccessResponse(activityVM, "Activity created successfully", 200)); } - return BadRequest(ApiResponse.ErrorResponse("Check List is Empty", "Check List is Empty", 400)); + ActivityVM activityVM = activityMaster.ToActivityVMFromActivityMaster(checkListVM); + + _logger.LogInfo("activity created successfully from tenant {tenantId}", tenantId); + return Ok(ApiResponse.SuccessResponse(activityVM, "activity created successfully", 200)); } - [HttpPost("edit/{id}")] + [HttpPost("activity/edit/{id}")] public async Task UpdateActivity(int id, [FromBody] CreateActivityMasterDto createActivity) { var tenantId = _userHelper.GetTenantId(); var employee = await _userHelper.GetCurrentEmployeeAsync(); ActivityMaster? activity = await _context.ActivityMasters.FirstOrDefaultAsync(x => x.Id == id && x.IsActive == true && x.TenantId == tenantId); - if (activity != null && createActivity.UnitOfMeasurement != null && createActivity.ActivityName != null) { + if (activity != null && createActivity.UnitOfMeasurement != null && createActivity.ActivityName != null) + { activity.ActivityName = createActivity.ActivityName; activity.UnitOfMeasurement = createActivity.UnitOfMeasurement; + List checkListVM = new List(); if (createActivity.CheckList != null) { List activityCheckList = new List(); foreach (var check in createActivity.CheckList) { - ActivityCheckList checkList = check.ToActivityCheckListFromCreateCheckListDto(tenantId,activity.Id); + ActivityCheckList checkList = check.ToActivityCheckListFromCreateCheckListDto(tenantId, activity.Id); activityCheckList.Add(checkList); } _context.ActivityCheckLists.UpdateRange(activityCheckList); await _context.SaveChangesAsync(); - List checkListVM = new List(); + foreach (ActivityCheckList check in activityCheckList) { var checkVM = check.ToCheckListVMFromActivityCheckList(activity.Id, false); checkListVM.Add(checkVM); } - - ActivityVM activityVM = activity.ToActivityVMFromActivityMaster(checkListVM); - return Ok(ApiResponse.SuccessResponse(activityVM, "Activity created successfully", 200)); } + ActivityVM activityVM = activity.ToActivityVMFromActivityMaster(checkListVM); + _logger.LogInfo("activity updated successfully from tenant {tenantId}", tenantId); + return Ok(ApiResponse.SuccessResponse(activityVM, "activity updated successfully", 200)); } - - return NotFound(ApiResponse.ErrorResponse("Activity no found", "Activity no found", 404)); + _logger.LogError("Activity {ActivityId} not found", id); + return NotFound(ApiResponse.ErrorResponse("Activity not found", "Activity not found", 404)); } -#nullable disable - [HttpDelete("delete/{id}")] + + [HttpDelete("activity/delete/{id}")] public async Task DeleteActivity(int Id) { int tenantId = _userHelper.GetTenantId(); var activity = await _context.ActivityMasters.FirstOrDefaultAsync(a => a.Id == Id && a.TenantId == tenantId); - if(activity != null) + if (activity != null) { activity.IsActive = false; } await _context.SaveChangesAsync(); - return Ok(ApiResponse.SuccessResponse(null, "Activity Deleted Successfully", 200)); + _logger.LogInfo("Activity Deleted Successfully from tenant {tenantId}", tenantId); + return Ok(ApiResponse.SuccessResponse(new { }, "Activity Deleted Successfully", 200)); } [HttpGet] [Route("industries")] @@ -137,8 +150,69 @@ namespace Marco.Pms.Services.Controllers { var tenantId = _userHelper.GetTenantId(); var industries = await _context.Industries.ToListAsync(); - - return Ok(ApiResponse.SuccessResponse(industries, "Success.", 200)); + + _logger.LogInfo("{count} industry records fetched successfully from tenant {tenantId}", industries.Count, tenantId); + return Ok(ApiResponse.SuccessResponse(industries, System.String.Format("{0} industry records fetched successfully", industries.Count), 200)); + } + + [HttpGet("ticket-status")] + public async Task GetTicketStatusMaster() + { + var tenantId = _userHelper.GetTenantId(); + List statusVMs = new List(); + List statusMasters = await _context.TicketStatusMasters.Where(s => s.TenantId == tenantId).ToListAsync(); + foreach (var statusMaster in statusMasters) + { + statusVMs.Add(statusMaster.ToTicketStatusVMFromTicketStatusMaster()); + } + + _logger.LogInfo("{count} Ticket Status records fetched successfully from tenant {tenantId}", statusVMs.Count, tenantId); + return Ok(ApiResponse.SuccessResponse(statusVMs, System.String.Format("{0} Ticket Status records fetched successfully", statusVMs.Count), 200)); + } + + [HttpGet("ticket-types")] + public async Task GetTicketTypeMaster() + { + var tenantId = _userHelper.GetTenantId(); + List typeVMs = new List(); + List typeMasters = await _context.TicketTypeMasters.Where(s => s.TenantId == tenantId).ToListAsync(); + foreach (var typeMaster in typeMasters) + { + typeVMs.Add(typeMaster.ToTicketTypeVMFromTicketTypeMaster()); + } + + _logger.LogInfo("{count} Ticket Type records fetched successfully from tenant {tenantId}", typeVMs.Count, tenantId); + return Ok(ApiResponse.SuccessResponse(typeVMs, System.String.Format("{0} Ticket Type records fetched successfully", typeVMs.Count), 200)); + } + + [HttpGet("ticket-priorities")] + public async Task GetTicketPriorityMaster() + { + var tenantId = _userHelper.GetTenantId(); + List priorityVMs = new List(); + List priorityMasters = await _context.TicketPriorityMasters.Where(s => s.TenantId == tenantId).ToListAsync(); + foreach (var priorityMaster in priorityMasters) + { + priorityVMs.Add(priorityMaster.ToTicketPriorityVMFromTicketPriorityMaster()); + } + + _logger.LogInfo("{count} Ticket Priority records fetched successfully from tenant {tenantId}", priorityVMs.Count, tenantId); + return Ok(ApiResponse.SuccessResponse(priorityVMs, System.String.Format("{0} Ticket Priority records fetched successfully", priorityVMs.Count), 200)); + } + + [HttpGet("ticket-tags")] + public async Task GetTicketTagMaster() + { + var tenantId = _userHelper.GetTenantId(); + List tagVMs = new List(); + List tagMasters = await _context.TicketTagMasters.Where(s => s.TenantId == tenantId).ToListAsync(); + foreach (var tagMaster in tagMasters) + { + tagVMs.Add(tagMaster.ToTicketTagVMFromTicketTagMaster()); + } + + _logger.LogInfo("{count} Ticket Tag records fetched successfully from tenant {tenantId}", tagVMs.Count, tenantId); + return Ok(ApiResponse.SuccessResponse(tagVMs, System.String.Format("{0} Ticket Tag records fetched successfully", tagVMs.Count), 200)); } } } diff --git a/Marco.Pms.Services/Service/S3UploadService.cs b/Marco.Pms.Services/Service/S3UploadService.cs index 8ef874b..cea43ea 100644 --- a/Marco.Pms.Services/Service/S3UploadService.cs +++ b/Marco.Pms.Services/Service/S3UploadService.cs @@ -38,11 +38,18 @@ namespace Marco.Pms.Services.Service ContentType = contentType, AutoCloseStream = true }; - - var transferUtility = new TransferUtility(_s3Client); - await transferUtility.UploadAsync(uploadRequest); - _logger.LogInfo("File uploaded to Amazon S3"); - return objectKey; + try + { + var transferUtility = new TransferUtility(_s3Client); + await transferUtility.UploadAsync(uploadRequest); + _logger.LogInfo("File uploaded to Amazon S3"); + return objectKey; + } + catch (Exception ex) + { + _logger.LogError("{error} while uploading file to S3", ex.Message); + return string.Empty; + } } public async Task GeneratePreSignedUrlAsync(string objectKey) { @@ -54,10 +61,37 @@ namespace Marco.Pms.Services.Service Expires = DateTime.UtcNow.AddMinutes(expiresInMinutes), Verb = HttpVerb.GET // for download }; + try + { + string url = _s3Client.GetPreSignedURL(request); + _logger.LogInfo("Requested presigned url from Amazon S3"); + return url; + } + catch (Exception ex) + { + _logger.LogError("{error} while requesting presigned url from Amazon S3", ex.Message); + return string.Empty; + } + } + public async Task DeleteFileAsync(string objectKey) + { + try + { + var deleteRequest = new DeleteObjectRequest + { + BucketName = _bucketName, + Key = objectKey + }; - string url = _s3Client.GetPreSignedURL(request); - _logger.LogInfo("Requested presigned url from Amazon S3"); - return url; + var response = await _s3Client.DeleteObjectAsync(deleteRequest); + _logger.LogInfo("File deleted from Amazon S3"); + return response.HttpStatusCode == System.Net.HttpStatusCode.NoContent; + } + catch (Exception ex) + { + _logger.LogError("{error} while deleting from Amazon S3", ex.Message); + return false; + } } public string GenerateFileName(string contentType, int tenantId, string? name) {