Initial commit: HelpDesk ICHN - .NET 10 backend + Next.js 16 frontend

This commit is contained in:
Nurfog committed 2026-09-10 10:20:51 -03:00
commit 359f0caf85
90 files changed
+13395

No files matched your search

@@ -0,0 +1,204 @@
using System.Security.Claims;
using HelpDesk.Data.Repositories;
using HelpDesk.Models.DTOs;
using HelpDesk.Models.Entities;
using HelpDesk.Models.Enums;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HelpDesk.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class TicketsController : ControllerBase
{
private readonly ITicketRepository _ticketRepository;
private readonly IUserRepository _userRepository;
public TicketsController(ITicketRepository ticketRepository, IUserRepository userRepository)
{
_ticketRepository = ticketRepository;
_userRepository = userRepository;
}
[HttpGet]
public async Task<ActionResult<TicketListResponse>> GetTickets(
[FromQuery] TicketStatus? status,
[FromQuery] TicketPriority? priority,
[FromQuery] Guid? assigneeId,
[FromQuery] Guid? requesterId,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var userId = GetUserId();
if (userId == null) return Unauthorized();
var user = await _userRepository.GetByIdAsync(userId.Value);
if (user == null) return Unauthorized();
if (user.Role == UserRole.Customer)
requesterId = userId.Value;
var tickets = await _ticketRepository.GetAllAsync(status, priority, assigneeId, requesterId, page, pageSize);
var totalCount = await _ticketRepository.GetCountAsync(status, priority, assigneeId, requesterId);
return Ok(new TicketListResponse
{
Items = tickets.Select(MapToDto).ToList(),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
});
}
[HttpGet("stats")]
public async Task<ActionResult<TicketStatsDto>> GetStats()
{
return Ok(new TicketStatsDto
{
TotalTickets = await _ticketRepository.GetTotalCountAsync(),
OpenTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.Open),
InProgressTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.InProgress),
PendingTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.Pending),
ResolvedTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.Resolved),
ClosedTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.Closed)
});
}
[HttpGet("{id}")]
public async Task<ActionResult<TicketDto>> GetTicket(Guid id)
{
var ticket = await _ticketRepository.GetByIdAsync(id);
if (ticket == null) return NotFound(new { error = "Ticket not found." });
var userId = GetUserId();
if (userId == null) return Unauthorized();
var user = await _userRepository.GetByIdAsync(userId.Value);
if (user?.Role == UserRole.Customer && ticket.RequesterId != userId.Value)
return Forbid();
return Ok(MapToDto(ticket));
}
[HttpPost]
public async Task<ActionResult<TicketDto>> CreateTicket([FromBody] CreateTicketRequest request)
{
if (string.IsNullOrWhiteSpace(request.Title))
return BadRequest(new { error = "Title is required." });
if (string.IsNullOrWhiteSpace(request.Description))
return BadRequest(new { error = "Description is required." });
var userId = GetUserId();
if (userId == null) return Unauthorized();
var ticket = new Ticket
{
Title = request.Title,
Description = request.Description,
Priority = request.Priority,
RequesterId = userId.Value,
GroupId = request.GroupId,
SlaPolicyId = request.SlaPolicyId,
TicketNumber = $"TK-{DateTime.UtcNow:yyyyMMddHHmmss}",
Status = TicketStatus.Open,
CreatedAt = DateTime.UtcNow
};
var created = await _ticketRepository.CreateAsync(ticket);
var full = await _ticketRepository.GetByIdAsync(created.Id);
return CreatedAtAction(nameof(GetTicket), new { id = created.Id }, MapToDto(full!));
}
[HttpPut("{id}")]
public async Task<ActionResult<TicketDto>> UpdateTicket(Guid id, [FromBody] UpdateTicketRequest request)
{
var ticket = await _ticketRepository.GetByIdAsync(id);
if (ticket == null) return NotFound(new { error = "Ticket not found." });
if (!string.IsNullOrWhiteSpace(request.Title)) ticket.Title = request.Title;
if (!string.IsNullOrWhiteSpace(request.Description)) ticket.Description = request.Description;
if (request.Priority.HasValue) ticket.Priority = request.Priority.Value;
var updated = await _ticketRepository.UpdateAsync(ticket);
if (updated == null) return StatusCode(500, new { error = "Failed to update ticket." });
var full = await _ticketRepository.GetByIdAsync(id);
return Ok(MapToDto(full!));
}
[HttpDelete("{id}")]
[Authorize(Policy = "Admin")]
public async Task<IActionResult> DeleteTicket(Guid id)
{
var deleted = await _ticketRepository.DeleteAsync(id);
if (!deleted) return NotFound(new { error = "Ticket not found." });
return NoContent();
}
[HttpPut("{id}/status")]
[Authorize(Policy = "Agent")]
public async Task<IActionResult> UpdateStatus(Guid id, [FromBody] UpdateStatusRequest request)
{
var ticket = await _ticketRepository.GetByIdAsync(id);
if (ticket == null) return NotFound(new { error = "Ticket not found." });
ticket.Status = request.Status;
await _ticketRepository.UpdateAsync(ticket);
return Ok(new { message = "Status updated.", status = request.Status.ToString() });
}
[HttpPut("{id}/assign")]
[Authorize(Policy = "Agent")]
public async Task<IActionResult> AssignTicket(Guid id, [FromBody] AssignTicketRequest request)
{
var ticket = await _ticketRepository.GetByIdAsync(id);
if (ticket == null) return NotFound(new { error = "Ticket not found." });
if (request.AssigneeId.HasValue)
{
var assignee = await _userRepository.GetByIdAsync(request.AssigneeId.Value);
if (assignee == null) return BadRequest(new { error = "Assignee not found." });
if (assignee.Role != UserRole.Agent && assignee.Role != UserRole.Admin)
return BadRequest(new { error = "Assignee must be an agent or admin." });
}
ticket.AssigneeId = request.AssigneeId;
if (request.AssigneeId.HasValue && ticket.Status == TicketStatus.Open)
ticket.Status = TicketStatus.InProgress;
await _ticketRepository.UpdateAsync(ticket);
return Ok(new { message = "Ticket assigned.", assigneeId = request.AssigneeId });
}
private Guid? GetUserId()
{
var claim = User.FindFirst("userId") ?? User.FindFirst(ClaimTypes.NameIdentifier);
return claim != null && Guid.TryParse(claim.Value, out var id) ? id : null;
}
private static TicketDto MapToDto(Ticket t) => new()
{
Id = t.Id,
Title = t.Title,
Description = t.Description,
Status = t.Status,
Priority = t.Priority,
TicketNumber = t.TicketNumber,
RequesterId = t.RequesterId,
RequesterName = t.Requester != null ? $"{t.Requester.FirstName} {t.Requester.LastName}" : "Unknown",
AssigneeId = t.AssigneeId,
AssigneeName = t.Assignee != null ? $"{t.Assignee.FirstName} {t.Assignee.LastName}" : null,
GroupId = t.GroupId,
GroupName = t.Group?.Name,
SlaPolicyId = t.SlaPolicyId,
SlaPolicyName = t.SlaPolicy?.Name,
CreatedAt = t.CreatedAt,
UpdatedAt = t.UpdatedAt,
ResolvedAt = t.ResolvedAt,
CommentCount = t.Comments?.Count ?? 0
};
}