Implemented core forum features including ticket creation, ticket updates, adding and updating comments, managing attachments, retrieving ticket details by ID, and fetching the ticket list

This commit is contained in:
ashutosh.nehete 2025-04-25 17:41:44 +05:30
parent fce124ac8b
commit ace2fe7d54
19 changed files with 1244 additions and 89 deletions

View File

@ -1,12 +1,14 @@
using System; namespace Marco.Pms.Model.Dtos.Forum
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Marco.Pms.Model.Dtos.Forum
{ {
public class AddCommentDto 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<ForumAttachmentDto>? Attachments { get; set; }
public int TenantId { get; set; }
} }
} }

View File

@ -2,5 +2,17 @@
{ {
public class CreateTicketDto 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<ForumAttachmentDto>? Attachments { get; set; }
public Guid PriorityId { get; set; }
public ICollection<Guid>? TagIds { get; set; }
public int TenantId { get; set; }
} }
} }

View File

@ -1,12 +1,14 @@
using System; namespace Marco.Pms.Model.Dtos.Forum
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Marco.Pms.Model.Dtos.Forum
{ {
public class ForumAttachmentDto 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; }
} }
} }

View File

@ -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<ForumAttachmentDto>? Attachments { get; set; }
public int TenantId { get; set; }
}
}

View File

@ -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<ForumAttachmentDto>? Attachments { get; set; }
public Guid PriorityId { get; set; }
public ICollection<Guid>? TagIds { get; set; }
public int TenantId { get; set; }
}
}

View File

@ -11,7 +11,7 @@ namespace Marco.Pms.Model.Forum
[ValidateNever] [ValidateNever]
[ForeignKey(nameof(TicketId))] [ForeignKey(nameof(TicketId))]
public TicketForum? Ticket { get; set; } public TicketForum? Ticket { get; set; }
public Guid CommentId { get; set; } public Guid? CommentId { get; set; }
[ValidateNever] [ValidateNever]
[ForeignKey(nameof(CommentId))] [ForeignKey(nameof(CommentId))]
public TicketComment? TicketComment { get; set; } public TicketComment? TicketComment { get; set; }

View File

@ -4,10 +4,10 @@
{ {
public int Id { get; set; } public int Id { get; set; }
public Guid TicketId { 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 Guid TagId { get; set; }
public TicketTagMaster Tag { get; set; } = new TicketTagMaster(); public TicketTagMaster? Tag { get; set; }
} }
} }

View File

@ -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<TicketAttachmentVM>()
};
}
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
};
}
}
}

View File

@ -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; }
}
}

View File

@ -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 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<TicketCommentVM>? Comments { get; set; } // view model
public ICollection<TicketAttachmentVM>? Attachments { get; set; } // view model
public ICollection<TicketTagVM>? Tags { get; set; }
} }
} }

View File

@ -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;
}
}

View File

@ -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<TicketAttachmentVM>? Attachments { get; set; }
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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; }
}
}

View File

@ -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
}
}

View File

@ -1,73 +1,796 @@
using Marco.Pms.DataAccess.Data; 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.Forum;
using Marco.Pms.Model.Dtos.Util; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Industries; using Marco.Pms.Model.Forum;
using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Forum;
using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Marco.Pms.Services.Controllers namespace Marco.Pms.Services.Controllers
{ {
[Authorize]
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class ForumController : ControllerBase public class ForumController : ControllerBase
{ {
private readonly UserManager<IdentityUser> _userManager;
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly JwtSettings _jwtSettings; private readonly UserHelper _userHelper;
private readonly RefreshTokenService _refreshTokenService; private readonly S3UploadService _s3Service;
private readonly IEmailSender _emailSender; private readonly ILoggingService _logger;
private readonly IConfiguration _configuration;
private readonly EmployeeHelper _employeeHelper;
//string tenentId = "1"; //string tenentId = "1";
public ForumController(UserManager<IdentityUser> userManager, ApplicationDbContext context, JwtSettings jwtSettings, RefreshTokenService refreshTokenService, public ForumController(ApplicationDbContext context, S3UploadService s3Service, UserHelper userHelper, ILoggingService logger)
IEmailSender emailSender, IConfiguration configuration, EmployeeHelper employeeHelper)
{ {
_userManager = userManager;
_jwtSettings = jwtSettings;
_refreshTokenService = refreshTokenService;
_emailSender = emailSender;
_configuration = configuration;
_employeeHelper = employeeHelper;
_context = context; _context = context;
_userHelper = userHelper;
_s3Service = s3Service;
_logger = logger;
} }
[HttpPost("tickets")] [HttpPost("ticket")]
public async Task<IActionResult> CreateNewTicket([FromBody] CreateTicketDto createTicketDto) public async Task<IActionResult> CreateNewTicket([FromBody] CreateTicketDto createTicketDto)
{ {
return NotFound(ApiResponse<object>.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<object>.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<TicketAttachment> attachments = new List<TicketAttachment>();
List<TicketTag> ticketTags = new List<TicketTag>();
List<Document> documents = new List<Document>();
List<TicketTagVM> tagVMs = new List<TicketTagVM>();
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<object>.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<object>.ErrorResponse(ex.Message, ex, 400)); ;
} }
[HttpPost("tickets/{id}/comment")] 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<TicketTagMaster>? 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<TicketAttachmentVM> attachmentVMs = new List<TicketAttachmentVM>();
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<object>.SuccessResponse(ticketVM, "Ticket Created Successfully", 200));
}
_logger.LogWarning("Employee {EmployeeId} tries to create ticket in different Tenant", createTicketDto.CreatedById);
return Unauthorized(ApiResponse<object>.ErrorResponse("Not Authorized", "Not Authorized", 401));
}
[HttpPost("ticket/edit")]
public async Task<IActionResult> 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<object>.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<TicketAttachment> attachments = new List<TicketAttachment>();
List<TicketTag> ticketTags = new List<TicketTag>();
List<TicketTagVM> tagVMs = new List<TicketTagVM>();
List<TicketAttachment> 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<object>.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<object>.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<Document> 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<TicketTag> existingTicketTags = await _context.TicketTags.Where(t => t.TicketId == updateTicketDto.Id).ToListAsync();
List<TicketTagMaster>? 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<TicketComment> comments = await _context.TicketComments.Where(c => c.TicketId == ticket.Id).ToListAsync();
var authorIds = comments.Select(c => c.AuthorId.ToString()).ToList();
List<Employee>? 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<TicketCommentVM> commentVMs = new List<TicketCommentVM>();
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<Document> documents = await _context.Documents.Where(d => fileIds.Contains(d.Id)).ToListAsync();
List<TicketAttachmentVM> ticketAttachmentVMs = new List<TicketAttachmentVM>();
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<object>.SuccessResponse(ticketVM, "Ticket Updated Successfully", 200));
}
_logger.LogError("Ticket {TicketId} not Found in database", updateTicketDto.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
}
_logger.LogWarning("Employee {EmployeeId} tries to create ticket in different Tenant", updateTicketDto.CreatedById);
return Unauthorized(ApiResponse<object>.ErrorResponse("Not Authorized", "Not Authorized", 401));
}
[HttpPost("ticket/comment")]
public async Task<IActionResult> AddComment([FromBody] AddCommentDto addCommentDto) public async Task<IActionResult> AddComment([FromBody] AddCommentDto addCommentDto)
{ {
return NotFound(ApiResponse<object>.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<object>.ErrorResponse("Invalid data", errors, 400));
} }
[HttpPost("tickets/{id}/attachments")] int tenantId = _userHelper.GetTenantId();
public async Task<IActionResult> UploadAttachments([FromBody] ForumAttachmentDto forumAttachmentDto) List<TicketAttachment> attachments = new List<TicketAttachment>();
List<Document> documents = new List<Document>();
TicketComment comment = addCommentDto.ToTicketCommentFromAddCommentDto();
_context.TicketComments.Add(comment);
await _context.SaveChangesAsync();
if (addCommentDto.Attachments != null)
{ {
return NotFound(ApiResponse<object>.ErrorResponse("Industry not found.", "Industry not found.", 404)); 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<object>.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<object>.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<TicketAttachmentVM> attachmentVMs = new List<TicketAttachmentVM>();
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<object>.SuccessResponse(commentVM, "Comment Created Successfully", 200));
}
[HttpPost("ticket/comment/edit")]
public async Task<IActionResult> 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<object>.ErrorResponse("Invalid data", errors, 400));
} }
[HttpPatch("tickets/{id}/status")] int tenantId = _userHelper.GetTenantId();
public async Task<IActionResult> UpdateTicketStatus([FromBody] InquiryDto inquiryDto) List<TicketAttachment> attachments = new List<TicketAttachment>();
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<TicketAttachment> 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)
{ {
return NotFound(ApiResponse<object>.ErrorResponse("Industry not found.", "Industry not found.", 404)); 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<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
} }
[HttpGet("tickets/{id}")] 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<object>.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<Document> 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<TicketAttachmentVM> attachmentVMs = new List<TicketAttachmentVM>();
attachments = await _context.TicketAttachments.Where(a => a.CommentId == updateCommentDto.Id).ToListAsync();
var fileIds = attachments.Select(a => a.FileId).ToList();
List<Document> 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<object>.SuccessResponse(commentVM, "Comment Updated Successfully", 200));
}
[HttpPost("ticket/attachment")]
public async Task<IActionResult> UploadAttachments([FromBody] List<ForumAttachmentDto> forumAttachmentDtos)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogError("{error}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
int tenantId = _userHelper.GetTenantId();
List<TicketAttachmentVM> ticketAttachmentVMs = new List<TicketAttachmentVM>();
foreach (var forumAttachmentDto in forumAttachmentDtos)
{
byte[] fileBytes;
if (string.IsNullOrEmpty(forumAttachmentDto.Base64Data))
{
_logger.LogError("Base64 data is missing");
return BadRequest(ApiResponse<object>.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<object>.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<object>.SuccessResponse(ticketAttachmentVMs, "attechments added Successfully", 200));
}
[HttpPatch("ticket/status/{id}")]
public async Task<IActionResult> UpdateTicketStatus(Guid id, [FromQuery] Guid statusId)
{
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<TicketComment> comments = await _context.TicketComments.Where(c => c.TicketId == ticket.Id).ToListAsync();
var authorIds = comments.Select(c => c.AuthorId.ToString()).ToList();
List<Employee> 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<Guid> tagIds = ticketTags.Select(t => t.TagId).ToList();
List<TicketTagMaster>? tagMasters = await _context.TicketTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync();
List<TicketTagVM> tagVMs = new List<TicketTagVM>();
foreach (var ticketTag in tagMasters)
{
TicketTagVM tagVM = ticketTag.ToTicketTagVMFromTicketTagMaster();
tagVMs.Add(tagVM);
}
ticketVM.Tags = tagVMs;
List<TicketCommentVM> commentVMs = new List<TicketCommentVM>();
foreach (var comment in comments)
{
employee = employees.Find(e => e.ApplicationUserId == comment.AuthorId.ToString()) ?? new Employee();
commentVMs.Add(comment.ToTicketCommentVMFromTicketComment(employee));
}
List<TicketAttachment> attachments = await _context.TicketAttachments.Where(a => a.TicketId == id).ToListAsync();
List<Guid> fileIds = attachments.Select(a => a.FileId).ToList();
List<Document> documents = await _context.Documents.Where(d => fileIds.Contains(d.Id)).ToListAsync();
List<TicketAttachmentVM> ticketAttachmentVMs = new List<TicketAttachmentVM>();
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<object>.SuccessResponse(ticketVM, "Ticket Fetched Successfully", 200));
}
[HttpGet("ticket/{id}")]
public async Task<IActionResult> GetTicketDetail(Guid id) public async Task<IActionResult> GetTicketDetail(Guid id)
{ {
return NotFound(ApiResponse<object>.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<TicketComment> comments = await _context.TicketComments.Where(c => c.TicketId == ticket.Id).ToListAsync();
var authorIds = comments.Select(c => c.AuthorId.ToString()).ToList();
List<Employee> 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<Guid> tagIds = ticketTags.Select(t => t.TagId).ToList();
List<TicketTagMaster>? tagMasters = await _context.TicketTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync();
List<TicketTagVM> tagVMs = new List<TicketTagVM>();
foreach (var ticketTag in tagMasters)
{
TicketTagVM tagVM = ticketTag.ToTicketTagVMFromTicketTagMaster();
tagVMs.Add(tagVM);
}
ticketVM.Tags = tagVMs;
List<TicketCommentVM> commentVMs = new List<TicketCommentVM>();
foreach (var comment in comments)
{
employee = employees.Find(e => e.ApplicationUserId == comment.AuthorId.ToString()) ?? new Employee();
commentVMs.Add(comment.ToTicketCommentVMFromTicketComment(employee));
}
List<TicketAttachment> attachments = await _context.TicketAttachments.Where(a => a.TicketId == id).ToListAsync();
List<Guid> fileIds = attachments.Select(a => a.FileId).ToList();
List<Document> documents = await _context.Documents.Where(d => fileIds.Contains(d.Id)).ToListAsync();
List<TicketAttachmentVM> ticketAttachmentVMs = new List<TicketAttachmentVM>();
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<object>.SuccessResponse(ticketVM, "Ticket Fetched Successfully", 200));
} }
[HttpGet("tickets")] [HttpGet("tickets")]
public async Task<IActionResult> ListTickets([FromBody] InquiryDto inquiryDto) public async Task<IActionResult> ListTickets()
{ {
return NotFound(ApiResponse<object>.ErrorResponse("Industry not found.", "Industry not found.", 404)); int tenantId = _userHelper.GetTenantId();
List<TicketForum> 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<TicketComment> ticketComments = await _context.TicketComments.Where(c => ticketIds.Contains(c.TicketId)).ToListAsync();
var authorIds = ticketComments.Select(c => c.AuthorId.ToString()).ToList();
List<Employee> 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<Guid> tagIds = ticketTags.Select(t => t.TagId).ToList();
List<TicketTagMaster>? tagMasters = await _context.TicketTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync();
List<TicketAttachment> ticketAttachments = await _context.TicketAttachments.Where(a => ticketIds.Contains(a.TicketId)).ToListAsync();
List<Guid> documentIds = ticketAttachments.Select(a => a.FileId).ToList();
List<Document> documents = await _context.Documents.Where(d => documentIds.Contains(d.Id)).ToListAsync();
List<ForumTicketVM> ticketVMs = new List<ForumTicketVM>();
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<Guid> ticketTagIds = ticketTagMappers.Select(t => t.TagId).ToList();
List<TicketTagMaster> ticketTagMasters = tagMasters.Where(t => ticketTagIds.Contains(t.Id)).ToList();
List<TicketTagVM> tagVMs = new List<TicketTagVM>();
foreach (var ticketTag in ticketTagMasters)
{
TicketTagVM tagVM = ticketTag.ToTicketTagVMFromTicketTagMaster();
tagVMs.Add(tagVM);
}
List<TicketComment> comments = ticketComments.Where(c => c.TicketId == ticket.Id).ToList();
List<TicketCommentVM> commentVMs = new List<TicketCommentVM>();
foreach (var comment in comments)
{
employee = employees.Find(e => e.ApplicationUserId == comment.AuthorId.ToString()) ?? new Employee();
commentVMs.Add(comment.ToTicketCommentVMFromTicketComment(employee));
}
List<TicketAttachment> attachments = ticketAttachments.Where(a => a.TicketId == ticket.Id).ToList();
List<Guid> fileIds = attachments.Select(a => a.FileId).ToList();
List<Document> files = documents.Where(d => fileIds.Contains(d.Id)).ToList();
List<TicketAttachmentVM> ticketAttachmentVMs = new List<TicketAttachmentVM>();
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<object>.SuccessResponse(ticketVMs, System.String.Format("{0} tickets records fetched successfully", ticketVMs.Count), 200));
} }
} }
} }

View File

@ -1,10 +1,13 @@
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Activities; using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Forum;
using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Forum;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -18,10 +21,12 @@ namespace Marco.Pms.Services.Controllers
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
public MasterController(ApplicationDbContext context, UserHelper userHelper) private readonly ILoggingService _logger;
public MasterController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger)
{ {
_context = context; _context = context;
_userHelper = userHelper; _userHelper = userHelper;
_logger = logger;
} }
[HttpGet] [HttpGet]
[Route("activities")] [Route("activities")]
@ -30,7 +35,8 @@ namespace Marco.Pms.Services.Controllers
var tenantId = _userHelper.GetTenantId(); var tenantId = _userHelper.GetTenantId();
var activities = await _context.ActivityMasters.Where(c => c.TenantId == tenantId && c.IsActive == true).ToListAsync(); var activities = await _context.ActivityMasters.Where(c => c.TenantId == tenantId && c.IsActive == true).ToListAsync();
List<ActivityVM> activitiesVM = new List<ActivityVM>(); List<ActivityVM> activitiesVM = new List<ActivityVM>();
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(); var checkList = await _context.ActivityCheckLists.Where(c => c.TenantId == tenantId && c.ActivityId == activity.Id).ToListAsync();
List<CheckListVM> checkListVM = new List<CheckListVM>(); List<CheckListVM> checkListVM = new List<CheckListVM>();
if (checkList != null) if (checkList != null)
@ -45,7 +51,8 @@ namespace Marco.Pms.Services.Controllers
ActivityVM activityVM = activity.ToActivityVMFromActivityMaster(checkListVM); ActivityVM activityVM = activity.ToActivityVMFromActivityMaster(checkListVM);
activitiesVM.Add(activityVM); activitiesVM.Add(activityVM);
} }
return Ok(ApiResponse<object>.SuccessResponse(activitiesVM, "Success.", 200)); _logger.LogInfo("{count} activity records fetched successfully from tenant {tenantId}", activitiesVM.Count, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(activitiesVM, System.String.Format("{0} activity records fetched successfully", activitiesVM.Count), 200));
} }
[HttpPost("activity")] [HttpPost("activity")]
@ -55,13 +62,16 @@ namespace Marco.Pms.Services.Controllers
var employee = await _userHelper.GetCurrentEmployeeAsync(); var employee = await _userHelper.GetCurrentEmployeeAsync();
if (employee.TenantId != tenantId) if (employee.TenantId != tenantId)
{ {
_logger.LogWarning("User from tenant {employeeTenantId} tries to access data from tenant {tenantId}", employee.TenantId, tenantId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Current tenant did not match with user's tenant", "Current tenant did not match with user's tenant", 401)); return Unauthorized(ApiResponse<object>.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); var activityMaster = createActivity.ToActivityMasterFromCreateActivityMasterDto(tenantId);
_context.ActivityMasters.Add(activityMaster); _context.ActivityMasters.Add(activityMaster);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
List<CheckListVM> checkListVM = new List<CheckListVM>();
if (createActivity.CheckList != null) { if (createActivity.CheckList != null)
{
List<ActivityCheckList> activityCheckList = new List<ActivityCheckList>(); List<ActivityCheckList> activityCheckList = new List<ActivityCheckList>();
foreach (var check in createActivity.CheckList) foreach (var check in createActivity.CheckList)
{ {
@ -70,29 +80,31 @@ namespace Marco.Pms.Services.Controllers
} }
_context.ActivityCheckLists.AddRange(activityCheckList); _context.ActivityCheckLists.AddRange(activityCheckList);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
List<CheckListVM> checkListVM = new List<CheckListVM>();
foreach (ActivityCheckList check in activityCheckList) foreach (ActivityCheckList check in activityCheckList)
{ {
var checkVM = check.ToCheckListVMFromActivityCheckList(activityMaster.Id, false); var checkVM = check.ToCheckListVMFromActivityCheckList(activityMaster.Id, false);
checkListVM.Add(checkVM); checkListVM.Add(checkVM);
} }
}
ActivityVM activityVM = activityMaster.ToActivityVMFromActivityMaster(checkListVM); ActivityVM activityVM = activityMaster.ToActivityVMFromActivityMaster(checkListVM);
return Ok(ApiResponse<object>.SuccessResponse(activityVM, "Activity created successfully", 200));
} _logger.LogInfo("activity created successfully from tenant {tenantId}", tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse("Check List is Empty", "Check List is Empty", 400)); return Ok(ApiResponse<object>.SuccessResponse(activityVM, "activity created successfully", 200));
} }
[HttpPost("edit/{id}")] [HttpPost("activity/edit/{id}")]
public async Task<IActionResult> UpdateActivity(int id, [FromBody] CreateActivityMasterDto createActivity) public async Task<IActionResult> UpdateActivity(int id, [FromBody] CreateActivityMasterDto createActivity)
{ {
var tenantId = _userHelper.GetTenantId(); var tenantId = _userHelper.GetTenantId();
var employee = await _userHelper.GetCurrentEmployeeAsync(); var employee = await _userHelper.GetCurrentEmployeeAsync();
ActivityMaster? activity = await _context.ActivityMasters.FirstOrDefaultAsync(x => x.Id == id && x.IsActive == true && x.TenantId == tenantId); 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.ActivityName = createActivity.ActivityName;
activity.UnitOfMeasurement = createActivity.UnitOfMeasurement; activity.UnitOfMeasurement = createActivity.UnitOfMeasurement;
List<CheckListVM> checkListVM = new List<CheckListVM>();
if (createActivity.CheckList != null) if (createActivity.CheckList != null)
{ {
@ -104,22 +116,22 @@ namespace Marco.Pms.Services.Controllers
} }
_context.ActivityCheckLists.UpdateRange(activityCheckList); _context.ActivityCheckLists.UpdateRange(activityCheckList);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
List<CheckListVM> checkListVM = new List<CheckListVM>();
foreach (ActivityCheckList check in activityCheckList) foreach (ActivityCheckList check in activityCheckList)
{ {
var checkVM = check.ToCheckListVMFromActivityCheckList(activity.Id, false); var checkVM = check.ToCheckListVMFromActivityCheckList(activity.Id, false);
checkListVM.Add(checkVM); checkListVM.Add(checkVM);
} }
}
ActivityVM activityVM = activity.ToActivityVMFromActivityMaster(checkListVM); ActivityVM activityVM = activity.ToActivityVMFromActivityMaster(checkListVM);
return Ok(ApiResponse<object>.SuccessResponse(activityVM, "Activity created successfully", 200)); _logger.LogInfo("activity updated successfully from tenant {tenantId}", tenantId);
return Ok(ApiResponse<object>.SuccessResponse(activityVM, "activity updated successfully", 200));
} }
_logger.LogError("Activity {ActivityId} not found", id);
return NotFound(ApiResponse<object>.ErrorResponse("Activity not found", "Activity not found", 404));
} }
return NotFound(ApiResponse<object>.ErrorResponse("Activity no found", "Activity no found", 404)); [HttpDelete("activity/delete/{id}")]
}
#nullable disable
[HttpDelete("delete/{id}")]
public async Task<IActionResult> DeleteActivity(int Id) public async Task<IActionResult> DeleteActivity(int Id)
{ {
int tenantId = _userHelper.GetTenantId(); int tenantId = _userHelper.GetTenantId();
@ -129,7 +141,8 @@ namespace Marco.Pms.Services.Controllers
activity.IsActive = false; activity.IsActive = false;
} }
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return Ok(ApiResponse<object>.SuccessResponse(null, "Activity Deleted Successfully", 200)); _logger.LogInfo("Activity Deleted Successfully from tenant {tenantId}", tenantId);
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Activity Deleted Successfully", 200));
} }
[HttpGet] [HttpGet]
[Route("industries")] [Route("industries")]
@ -138,7 +151,68 @@ namespace Marco.Pms.Services.Controllers
var tenantId = _userHelper.GetTenantId(); var tenantId = _userHelper.GetTenantId();
var industries = await _context.Industries.ToListAsync(); var industries = await _context.Industries.ToListAsync();
return Ok(ApiResponse<object>.SuccessResponse(industries, "Success.", 200)); _logger.LogInfo("{count} industry records fetched successfully from tenant {tenantId}", industries.Count, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(industries, System.String.Format("{0} industry records fetched successfully", industries.Count), 200));
}
[HttpGet("ticket-status")]
public async Task<IActionResult> GetTicketStatusMaster()
{
var tenantId = _userHelper.GetTenantId();
List<TicketStatusVM> statusVMs = new List<TicketStatusVM>();
List<TicketStatusMaster> 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<object>.SuccessResponse(statusVMs, System.String.Format("{0} Ticket Status records fetched successfully", statusVMs.Count), 200));
}
[HttpGet("ticket-types")]
public async Task<IActionResult> GetTicketTypeMaster()
{
var tenantId = _userHelper.GetTenantId();
List<TicketTypeVM> typeVMs = new List<TicketTypeVM>();
List<TicketTypeMaster> 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<object>.SuccessResponse(typeVMs, System.String.Format("{0} Ticket Type records fetched successfully", typeVMs.Count), 200));
}
[HttpGet("ticket-priorities")]
public async Task<IActionResult> GetTicketPriorityMaster()
{
var tenantId = _userHelper.GetTenantId();
List<TicketPriorityVM> priorityVMs = new List<TicketPriorityVM>();
List<TicketPriorityMaster> 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<object>.SuccessResponse(priorityVMs, System.String.Format("{0} Ticket Priority records fetched successfully", priorityVMs.Count), 200));
}
[HttpGet("ticket-tags")]
public async Task<IActionResult> GetTicketTagMaster()
{
var tenantId = _userHelper.GetTenantId();
List<TicketTagVM> tagVMs = new List<TicketTagVM>();
List<TicketTagMaster> 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<object>.SuccessResponse(tagVMs, System.String.Format("{0} Ticket Tag records fetched successfully", tagVMs.Count), 200));
} }
} }
} }

View File

@ -38,12 +38,19 @@ namespace Marco.Pms.Services.Service
ContentType = contentType, ContentType = contentType,
AutoCloseStream = true AutoCloseStream = true
}; };
try
{
var transferUtility = new TransferUtility(_s3Client); var transferUtility = new TransferUtility(_s3Client);
await transferUtility.UploadAsync(uploadRequest); await transferUtility.UploadAsync(uploadRequest);
_logger.LogInfo("File uploaded to Amazon S3"); _logger.LogInfo("File uploaded to Amazon S3");
return objectKey; return objectKey;
} }
catch (Exception ex)
{
_logger.LogError("{error} while uploading file to S3", ex.Message);
return string.Empty;
}
}
public async Task<string> GeneratePreSignedUrlAsync(string objectKey) public async Task<string> GeneratePreSignedUrlAsync(string objectKey)
{ {
int expiresInMinutes = 1; int expiresInMinutes = 1;
@ -54,11 +61,38 @@ namespace Marco.Pms.Services.Service
Expires = DateTime.UtcNow.AddMinutes(expiresInMinutes), Expires = DateTime.UtcNow.AddMinutes(expiresInMinutes),
Verb = HttpVerb.GET // for download Verb = HttpVerb.GET // for download
}; };
try
{
string url = _s3Client.GetPreSignedURL(request); string url = _s3Client.GetPreSignedURL(request);
_logger.LogInfo("Requested presigned url from Amazon S3"); _logger.LogInfo("Requested presigned url from Amazon S3");
return url; return url;
} }
catch (Exception ex)
{
_logger.LogError("{error} while requesting presigned url from Amazon S3", ex.Message);
return string.Empty;
}
}
public async Task<bool> DeleteFileAsync(string objectKey)
{
try
{
var deleteRequest = new DeleteObjectRequest
{
BucketName = _bucketName,
Key = objectKey
};
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) public string GenerateFileName(string contentType, int tenantId, string? name)
{ {
string extenstion = contentType.Split("/")[1]; string extenstion = contentType.Split("/")[1];