chore: elimina backend standalone (vive en ICHN/sam) + README solo-frontend
This commit is contained in:
1 parent
ce008105c9
commit
5c36382600
52 files changed
+43
-2902
No files matched your search
@@ -1,176 +1,72 @@
|
||||
# HelpDesk ICHN
|
||||
# HelpDesk ICHN (Frontend)
|
||||
|
||||
Sistema de HelpDesk inspirado en Freshdesk/Zammad, construido con .NET 10 + Next.js 16 + PostgreSQL.
|
||||
Frontend Next.js del sistema de HelpDesk (estilo Freshdesk/Zammad).
|
||||
|
||||
## Arquitectura
|
||||
> **El backend vive en el proyecto principal**: `ICHN/sam` —
|
||||
> `backend/SAM.API/Controllers/HelpdeskController.cs` (ruta `/api/helpdesk/*`,
|
||||
> datos en schema `helpdesk.*` de la DB compartida). Este repo es **solo frontend**.
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
helpdesk/
|
||||
├── backend/ # .NET 10 API
|
||||
│ ├── HelpDesk.API/ # Web API (controllers, services, middleware)
|
||||
│ ├── HelpDesk.Data/ # EF Core DbContext + Repositories
|
||||
│ ├── HelpDesk.Models/ # Entities, Enums, DTOs
|
||||
│ └── HelpDesk.sln
|
||||
├── frontend/ # Next.js 16 + Bootstrap 5
|
||||
│ └── src/
|
||||
│ ├── app/ # App Router pages
|
||||
│ │ ├── (auth)/ # Login, Register
|
||||
│ │ └── (app)/ # Dashboard, Tickets, KB, Users
|
||||
│ ├── components/ # Reusable UI components
|
||||
│ │ ├── (auth)/ # Login
|
||||
│ │ └── (app)/ # Dashboard, Tickets, KB, Settings
|
||||
│ ├── components/ # UI reutilizable
|
||||
│ └── lib/ # API client, auth context
|
||||
├── gateway/ # Caddy reverse proxy
|
||||
├── scripts/ # DB init scripts
|
||||
├── docker-compose.yml # Full stack orchestration
|
||||
├── gateway/ # (legado, no se usa en deploy)
|
||||
├── scripts/ # (legado, no se usa en deploy)
|
||||
├── docker-compose.yml # Solo frontend (red sam-net)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Stack
|
||||
|
||||
| Componente | Tecnología |
|
||||
|--------------------|--------------------------|
|
||||
| Backend API | .NET 10, C# 14 |
|
||||
| ORM | Entity Framework Core 10 |
|
||||
| Database | PostgreSQL 16 |
|
||||
| Cache | Redis 7 |
|
||||
| Frontend | Next.js 16 (App Router) |
|
||||
| UI Framework | Bootstrap 5 + React-Bootstrap |
|
||||
| Authentication | JWT Bearer |
|
||||
| Reverse Proxy | Caddy 2 |
|
||||
| Icons | Bootstrap Icons |
|
||||
| Font | Fira Sans (matching SAM) |
|
||||
|
||||
## Paleta de Colores (SAM)
|
||||
|
||||
| Variable | Valor | Uso |
|
||||
|----------------|-----------|------------------------------|
|
||||
| Primary | #00285d | Navbar, sidebar, headings |
|
||||
| Primary Light | #1a3f7a | Hover states, gradients |
|
||||
| Gradient | #00285d → #1a4a8a | Sidebar, buttons, headers |
|
||||
| Background | #f0f2f5 | Page background |
|
||||
| Card Shadow | rgba(0,0,0,0.08) | Card elevation |
|
||||
| Componente | Tecnología |
|
||||
|--------------|-----------------------------------|
|
||||
| Frontend | Next.js 16 (App Router) |
|
||||
| UI | Bootstrap 5 + React-Bootstrap |
|
||||
| Auth | Bearer token propio del backend |
|
||||
| Icons / Font | Bootstrap Icons / Fira Sans |
|
||||
|
||||
## Desarrollo Local
|
||||
|
||||
### Prerequisitos
|
||||
- .NET 10 SDK
|
||||
- Node.js 24+
|
||||
- PostgreSQL 16 (o usar Docker)
|
||||
- Redis (o usar Docker)
|
||||
|
||||
### Backend
|
||||
```bash
|
||||
cd backend
|
||||
dotnet restore
|
||||
dotnet run --project HelpDesk.API
|
||||
# API en http://localhost:5000
|
||||
# Swagger en http://localhost:5000/swagger
|
||||
```
|
||||
|
||||
### Frontend
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
pnpm install
|
||||
pnpm dev
|
||||
# Frontend en http://localhost:3000
|
||||
```
|
||||
|
||||
### Docker (todo junto)
|
||||
```bash
|
||||
docker compose up -d
|
||||
# Caddy: http://localhost:8080
|
||||
# API: http://localhost:5000
|
||||
# Frontend: http://localhost:3000
|
||||
# PostgreSQL: localhost:5433
|
||||
# Redis: localhost:6381
|
||||
Variable de entorno (`frontend/.env.local`, no versionado):
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_API_URL=https://api.ichn.local # dev (.254)
|
||||
# NEXT_PUBLIC_API_URL=https://api.devops.norteamericano.cl # prod (.4)
|
||||
```
|
||||
|
||||
## Datos por Defecto
|
||||
Auth: login con `idUsuario` + password (valida contra `sige_sam_v3.usuario`
|
||||
en el backend). El primer integrante del equipo queda `admin` (bootstrap).
|
||||
|
||||
### Usuario Admin
|
||||
- Email: admin@helpdesk.local
|
||||
- Password: Admin123!
|
||||
- Rol: SuperAdmin
|
||||
## Deploy
|
||||
|
||||
### Grupos
|
||||
- Soporte General
|
||||
- Ventas
|
||||
- Desarrollo
|
||||
Lo publica el deployer de SAM desde este repo (módulo `helpdesk`):
|
||||
push a `main` → webhook Gitea → build + up en `.4`
|
||||
(`https://helpdesk.devops.norteamericano.cl`).
|
||||
|
||||
### Políticas SLA
|
||||
- Estándar: Primera respuesta 4h, Resolución 24h
|
||||
- Premium: Primera respuesta 1h, Resolución 8h
|
||||
## Endpoints que consume (backend SAM)
|
||||
|
||||
## Endpoints API Principales
|
||||
Base: `NEXT_PUBLIC_API_URL/api/helpdesk`
|
||||
|
||||
### Auth
|
||||
- `POST /api/auth/login` - Iniciar sesión
|
||||
- `POST /api/auth/register` - Registrar usuario
|
||||
- `GET /api/auth/me` - Usuario actual
|
||||
|
||||
### Tickets
|
||||
- `GET /api/tickets` - Listar tickets (filtros: status, priority, assigneeId, search)
|
||||
- `GET /api/tickets/{id}` - Detalle de ticket
|
||||
- `POST /api/tickets` - Crear ticket
|
||||
- `PUT /api/tickets/{id}` - Actualizar ticket
|
||||
- `DELETE /api/tickets/{id}` - Eliminar ticket
|
||||
- `PUT /api/tickets/{id}/status` - Cambiar estado
|
||||
- `PUT /api/tickets/{id}/assign` - Asignar agente
|
||||
- `POST /api/tickets/{id}/comments` - Agregar comentario
|
||||
- `GET /api/tickets/stats` - Estadísticas
|
||||
|
||||
### Knowledge Base
|
||||
- `GET /api/kb/articles/published` - Artículos publicados
|
||||
- `GET /api/kb/search?q=termo` - Buscar artículos
|
||||
|
||||
### Dashboard
|
||||
- `GET /api/dashboard/stats` - Estadísticas del dashboard
|
||||
|
||||
### Users
|
||||
- `GET /api/users` - Listar usuarios
|
||||
- `GET /api/users/agents` - Listar agentes
|
||||
|
||||
### Groups
|
||||
- `GET /api/groups` - Listar grupos
|
||||
|
||||
### SLA
|
||||
- `GET /api/sla` - Listar políticas SLA
|
||||
|
||||
## Funcionalidades
|
||||
|
||||
### Gestión de Tickets
|
||||
- Crear, editar, eliminar tickets
|
||||
- Estados: Abierto → En Progreso → Pendiente (Cliente/Agente) → Resuelto → Cerrado
|
||||
- Prioridades: Baja, Media, Alta, Urgente
|
||||
- Asignación a agentes y grupos
|
||||
- Comentarios públicos e internos
|
||||
- Historial de actividad
|
||||
- Número de ticket automático (#XXXX)
|
||||
|
||||
### Dashboard
|
||||
- Resumen de tickets por estado
|
||||
- Distribución por prioridad
|
||||
- Tiempo promedio de respuesta
|
||||
- Carga de trabajo por agente
|
||||
- Tickets recientes
|
||||
|
||||
### Knowledge Base
|
||||
- Artículos organizados por categorías
|
||||
- Búsqueda de artículos
|
||||
- Contador de vistas
|
||||
- Publicar/ocultar artículos
|
||||
|
||||
### Autenticación
|
||||
- JWT Bearer tokens
|
||||
- Roles: SuperAdmin, Admin, Agente, Cliente
|
||||
- Registro de nuevos usuarios
|
||||
- Políticas de autorización por rol
|
||||
|
||||
## Diseño
|
||||
|
||||
Inspirado en la interfaz de SAM (sam.norteamericano.cl):
|
||||
- Sidebar azul oscuro con gradiente
|
||||
- Tarjetas blancas con sombras suaves
|
||||
- Fondo gris claro
|
||||
- Tipografía Fira Sans
|
||||
- Badges de color para estados y prioridades
|
||||
- Layout responsive (mobile-first)
|
||||
- `POST /login {idUsuario, password}` · `GET /me`
|
||||
- `GET /tickets?...` · `GET /ticket/{id}` · `POST /tickets` · `PUT /ticket/{id}` (+ `/status`, `/assign`) · `DELETE /ticket/{id}`
|
||||
- `GET|POST /comments/{ticketId}`
|
||||
- `GET /kb/published` · `GET /kb/search?q=` · `GET /kb/categories`
|
||||
- `GET /stats`
|
||||
- `GET|POST /groups` · `PUT|DELETE /groups/{id}` (admin)
|
||||
- `GET|POST /users` · `PUT /users/{id}` (admin)
|
||||
- `GET|POST /sla` · `PUT /sla/{id}` (admin)
|
||||
- `GET /email/templates` · `PUT /email/templates/{key}` · `GET /email/log` · `POST /email/test` (admin)
|
||||
@@ -1,22 +0,0 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy solution and project files for layer caching
|
||||
COPY backend/HelpDesk.sln backend/HelpDesk.sln
|
||||
COPY backend/HelpDesk.Models/HelpDesk.Models.csproj backend/HelpDesk.Models/
|
||||
COPY backend/HelpDesk.Data/HelpDesk.Data.csproj backend/HelpDesk.Data/
|
||||
COPY backend/HelpDesk.API/HelpDesk.API.csproj backend/HelpDesk.API/
|
||||
RUN dotnet restore backend/HelpDesk.API/HelpDesk.API.csproj
|
||||
|
||||
# Copy everything and build
|
||||
COPY backend/ backend/
|
||||
WORKDIR /src/backend
|
||||
RUN dotnet publish HelpDesk.API/HelpDesk.API.csproj -c Release -o /app/publish --no-restore
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
EXPOSE 5000
|
||||
ENV ASPNETCORE_URLS=http://+:5000
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
ENTRYPOINT ["dotnet", "HelpDesk.API.dll"]
|
||||
@@ -1,115 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using HelpDesk.API.Services;
|
||||
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]")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IJwtService _jwtService;
|
||||
private readonly IPasswordService _passwordService;
|
||||
|
||||
public AuthController(IUserRepository userRepository, IJwtService jwtService, IPasswordService passwordService)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_jwtService = jwtService;
|
||||
_passwordService = passwordService;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<ActionResult<AuthResponse>> Login([FromBody] LoginRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password))
|
||||
return BadRequest(new { error = "Email and password are required." });
|
||||
|
||||
var user = await _userRepository.GetByEmailAsync(request.Email);
|
||||
if (user == null || !_passwordService.VerifyPassword(request.Password, user.PasswordHash))
|
||||
return Unauthorized(new { error = "Invalid email or password." });
|
||||
|
||||
if (!user.IsActive)
|
||||
return Unauthorized(new { error = "Account is deactivated." });
|
||||
|
||||
var token = _jwtService.GenerateToken(user.Id, user.Email, user.Role);
|
||||
|
||||
return Ok(new AuthResponse
|
||||
{
|
||||
Token = token,
|
||||
Expiration = DateTime.UtcNow.AddMinutes(60),
|
||||
User = MapToDto(user)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<ActionResult<AuthResponse>> Register([FromBody] RegisterRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password))
|
||||
return BadRequest(new { error = "Email and password are required." });
|
||||
if (string.IsNullOrWhiteSpace(request.FirstName) || string.IsNullOrWhiteSpace(request.LastName))
|
||||
return BadRequest(new { error = "First name and last name are required." });
|
||||
if (request.Password.Length < 6)
|
||||
return BadRequest(new { error = "Password must be at least 6 characters." });
|
||||
|
||||
if (await _userRepository.ExistsAsync(request.Email))
|
||||
return Conflict(new { error = "Email is already registered." });
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Email = request.Email,
|
||||
PasswordHash = _passwordService.HashPassword(request.Password),
|
||||
FirstName = request.FirstName,
|
||||
LastName = request.LastName,
|
||||
Role = UserRole.Customer,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var created = await _userRepository.CreateAsync(user);
|
||||
var token = _jwtService.GenerateToken(created.Id, created.Email, created.Role);
|
||||
|
||||
return CreatedAtAction(nameof(GetMe), new AuthResponse
|
||||
{
|
||||
Token = token,
|
||||
Expiration = DateTime.UtcNow.AddMinutes(60),
|
||||
User = MapToDto(created)
|
||||
});
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("me")]
|
||||
public async Task<ActionResult<UserDto>> GetMe()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(userId.Value);
|
||||
if (user == null) return NotFound(new { error = "User not found." });
|
||||
|
||||
return Ok(MapToDto(user));
|
||||
}
|
||||
|
||||
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 UserDto MapToDto(User user) => new()
|
||||
{
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
Role = user.Role,
|
||||
IsActive = user.IsActive,
|
||||
CreatedAt = user.CreatedAt,
|
||||
PhoneNumber = user.PhoneNumber,
|
||||
AvatarUrl = user.AvatarUrl
|
||||
};
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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(Policy = "Agent")]
|
||||
public class DashboardController : ControllerBase
|
||||
{
|
||||
private readonly ITicketRepository _ticketRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public DashboardController(ITicketRepository ticketRepository, IUserRepository userRepository)
|
||||
{
|
||||
_ticketRepository = ticketRepository;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
[HttpGet("stats")]
|
||||
public async Task<ActionResult<DashboardStatsDto>> GetStats()
|
||||
{
|
||||
var agents = await _userRepository.GetByRoleAsync(UserRole.Agent);
|
||||
var agentWorkloads = new List<AgentWorkloadDto>();
|
||||
|
||||
foreach (var agent in agents)
|
||||
{
|
||||
var activeTickets = await _ticketRepository.GetCountAsync(
|
||||
assigneeId: agent.Id,
|
||||
status: TicketStatus.Open);
|
||||
|
||||
var inProgressTickets = await _ticketRepository.GetCountAsync(
|
||||
assigneeId: agent.Id,
|
||||
status: TicketStatus.InProgress);
|
||||
|
||||
agentWorkloads.Add(new AgentWorkloadDto
|
||||
{
|
||||
UserId = agent.Id,
|
||||
UserName = $"{agent.FirstName} {agent.LastName}",
|
||||
ActiveTickets = activeTickets + inProgressTickets,
|
||||
ResolvedThisWeek = 0
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new DashboardStatsDto
|
||||
{
|
||||
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),
|
||||
CriticalTickets = await _ticketRepository.GetCountByPriorityAsync(TicketPriority.Critical),
|
||||
HighTickets = await _ticketRepository.GetCountByPriorityAsync(TicketPriority.High),
|
||||
MediumTickets = await _ticketRepository.GetCountByPriorityAsync(TicketPriority.Medium),
|
||||
LowTickets = await _ticketRepository.GetCountByPriorityAsync(TicketPriority.Low),
|
||||
AverageResponseTimeMinutes = await _ticketRepository.GetAverageResponseTimeAsync(),
|
||||
AverageResolutionTimeMinutes = await _ticketRepository.GetAverageResolutionTimeAsync(),
|
||||
AgentWorkloads = agentWorkloads
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
using HelpDesk.Data.Repositories;
|
||||
using HelpDesk.Models.DTOs;
|
||||
using HelpDesk.Models.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HelpDesk.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public class GroupsController : ControllerBase
|
||||
{
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
|
||||
public GroupsController(IGroupRepository groupRepository)
|
||||
{
|
||||
_groupRepository = groupRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<GroupDto>>> GetGroups()
|
||||
{
|
||||
var groups = await _groupRepository.GetAllAsync();
|
||||
return Ok(groups.Select(MapToDto));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<GroupDto>> GetGroup(Guid id)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(id);
|
||||
if (group == null) return NotFound(new { error = "Group not found." });
|
||||
return Ok(MapToDto(group));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<GroupDto>> CreateGroup([FromBody] CreateGroupRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
return BadRequest(new { error = "Name is required." });
|
||||
|
||||
var group = new Group
|
||||
{
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var created = await _groupRepository.CreateAsync(group);
|
||||
return CreatedAtAction(nameof(GetGroup), new { id = created.Id }, MapToDto(created));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<GroupDto>> UpdateGroup(Guid id, [FromBody] CreateGroupRequest request)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(id);
|
||||
if (group == null) return NotFound(new { error = "Group not found." });
|
||||
|
||||
group.Name = request.Name;
|
||||
if (request.Description != null) group.Description = request.Description;
|
||||
|
||||
var updated = await _groupRepository.UpdateAsync(group);
|
||||
if (updated == null) return StatusCode(500, new { error = "Failed to update group." });
|
||||
|
||||
return Ok(MapToDto(updated));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteGroup(Guid id)
|
||||
{
|
||||
var deleted = await _groupRepository.DeleteAsync(id);
|
||||
if (!deleted) return NotFound(new { error = "Group not found." });
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static GroupDto MapToDto(Group g) => new()
|
||||
{
|
||||
Id = g.Id,
|
||||
Name = g.Name,
|
||||
Description = g.Description,
|
||||
IsActive = g.IsActive,
|
||||
CreatedAt = g.CreatedAt
|
||||
};
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using HelpDesk.Data.Repositories;
|
||||
using HelpDesk.Models.DTOs;
|
||||
using HelpDesk.Models.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HelpDesk.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/kb")]
|
||||
[Authorize]
|
||||
public class KnowledgeBaseController : ControllerBase
|
||||
{
|
||||
private readonly IKnowledgeBaseRepository _kbRepository;
|
||||
|
||||
public KnowledgeBaseController(IKnowledgeBaseRepository kbRepository)
|
||||
{
|
||||
_kbRepository = kbRepository;
|
||||
}
|
||||
|
||||
// ===== Articles =====
|
||||
|
||||
[HttpGet("articles")]
|
||||
[Authorize(Policy = "Agent")]
|
||||
public async Task<ActionResult<IEnumerable<ArticleDto>>> GetAllArticles()
|
||||
{
|
||||
var articles = await _kbRepository.GetAllArticlesAsync();
|
||||
return Ok(articles.Select(MapArticleToDto));
|
||||
}
|
||||
|
||||
[HttpGet("articles/published")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<IEnumerable<ArticleDto>>> GetPublishedArticles()
|
||||
{
|
||||
var articles = await _kbRepository.GetPublishedArticlesAsync();
|
||||
return Ok(articles.Select(MapArticleToDto));
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<IEnumerable<ArticleDto>>> SearchArticles([FromQuery] string q)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
return BadRequest(new { error = "Search query is required." });
|
||||
|
||||
var articles = await _kbRepository.SearchArticlesAsync(q);
|
||||
return Ok(articles.Select(MapArticleToDto));
|
||||
}
|
||||
|
||||
[HttpGet("articles/{id}")]
|
||||
public async Task<ActionResult<ArticleDto>> GetArticle(Guid id)
|
||||
{
|
||||
var article = await _kbRepository.GetArticleByIdAsync(id);
|
||||
if (article == null) return NotFound(new { error = "Article not found." });
|
||||
return Ok(MapArticleToDto(article));
|
||||
}
|
||||
|
||||
[HttpPost("articles")]
|
||||
[Authorize(Policy = "Agent")]
|
||||
public async Task<ActionResult<ArticleDto>> CreateArticle([FromBody] CreateArticleRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Title))
|
||||
return BadRequest(new { error = "Title is required." });
|
||||
if (string.IsNullOrWhiteSpace(request.Content))
|
||||
return BadRequest(new { error = "Content is required." });
|
||||
|
||||
var userId = GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var article = new KnowledgeBaseArticle
|
||||
{
|
||||
Title = request.Title,
|
||||
Content = request.Content,
|
||||
CategoryId = request.CategoryId,
|
||||
IsPublished = request.IsPublished,
|
||||
AuthorId = userId.Value,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var created = await _kbRepository.CreateArticleAsync(article);
|
||||
var full = await _kbRepository.GetArticleByIdAsync(created.Id);
|
||||
return CreatedAtAction(nameof(GetArticle), new { id = created.Id }, MapArticleToDto(full!));
|
||||
}
|
||||
|
||||
[HttpPut("articles/{id}")]
|
||||
[Authorize(Policy = "Agent")]
|
||||
public async Task<ActionResult<ArticleDto>> UpdateArticle(Guid id, [FromBody] UpdateArticleRequest request)
|
||||
{
|
||||
var article = await _kbRepository.GetArticleByIdAsync(id);
|
||||
if (article == null) return NotFound(new { error = "Article not found." });
|
||||
|
||||
if (request.Title != null) article.Title = request.Title;
|
||||
if (request.Content != null) article.Content = request.Content;
|
||||
if (request.CategoryId.HasValue) article.CategoryId = request.CategoryId.Value;
|
||||
if (request.IsPublished.HasValue) article.IsPublished = request.IsPublished.Value;
|
||||
|
||||
var updated = await _kbRepository.UpdateArticleAsync(article);
|
||||
if (updated == null) return StatusCode(500, new { error = "Failed to update article." });
|
||||
|
||||
var full = await _kbRepository.GetArticleByIdAsync(id);
|
||||
return Ok(MapArticleToDto(full!));
|
||||
}
|
||||
|
||||
[HttpDelete("articles/{id}")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<IActionResult> DeleteArticle(Guid id)
|
||||
{
|
||||
var deleted = await _kbRepository.DeleteArticleAsync(id);
|
||||
if (!deleted) return NotFound(new { error = "Article not found." });
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// ===== Categories =====
|
||||
|
||||
[HttpGet("categories")]
|
||||
public async Task<ActionResult<IEnumerable<CategoryDto>>> GetCategories()
|
||||
{
|
||||
var categories = await _kbRepository.GetAllCategoriesAsync();
|
||||
return Ok(categories.Select(MapCategoryToDto));
|
||||
}
|
||||
|
||||
[HttpGet("categories/{id}")]
|
||||
public async Task<ActionResult<CategoryDto>> GetCategory(Guid id)
|
||||
{
|
||||
var category = await _kbRepository.GetCategoryByIdAsync(id);
|
||||
if (category == null) return NotFound(new { error = "Category not found." });
|
||||
return Ok(MapCategoryToDto(category));
|
||||
}
|
||||
|
||||
[HttpPost("categories")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ActionResult<CategoryDto>> CreateCategory([FromBody] CreateCategoryRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
return BadRequest(new { error = "Name is required." });
|
||||
|
||||
var category = new KnowledgeBaseCategory
|
||||
{
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
SortOrder = request.SortOrder,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var created = await _kbRepository.CreateCategoryAsync(category);
|
||||
return CreatedAtAction(nameof(GetCategory), new { id = created.Id }, MapCategoryToDto(created));
|
||||
}
|
||||
|
||||
[HttpPut("categories/{id}")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ActionResult<CategoryDto>> UpdateCategory(Guid id, [FromBody] CreateCategoryRequest request)
|
||||
{
|
||||
var category = await _kbRepository.GetCategoryByIdAsync(id);
|
||||
if (category == null) return NotFound(new { error = "Category not found." });
|
||||
|
||||
category.Name = request.Name;
|
||||
category.Description = request.Description;
|
||||
category.SortOrder = request.SortOrder;
|
||||
|
||||
var updated = await _kbRepository.UpdateCategoryAsync(category);
|
||||
if (updated == null) return StatusCode(500, new { error = "Failed to update category." });
|
||||
|
||||
return Ok(MapCategoryToDto(updated));
|
||||
}
|
||||
|
||||
[HttpDelete("categories/{id}")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<IActionResult> DeleteCategory(Guid id)
|
||||
{
|
||||
var deleted = await _kbRepository.DeleteCategoryAsync(id);
|
||||
if (!deleted) return NotFound(new { error = "Category not found." });
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
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 ArticleDto MapArticleToDto(KnowledgeBaseArticle a) => new()
|
||||
{
|
||||
Id = a.Id,
|
||||
Title = a.Title,
|
||||
Content = a.Content,
|
||||
IsPublished = a.IsPublished,
|
||||
CategoryId = a.CategoryId,
|
||||
CategoryName = a.Category?.Name ?? "Unknown",
|
||||
AuthorId = a.AuthorId,
|
||||
AuthorName = a.Author != null ? $"{a.Author.FirstName} {a.Author.LastName}" : "Unknown",
|
||||
ViewCount = a.ViewCount,
|
||||
CreatedAt = a.CreatedAt,
|
||||
UpdatedAt = a.UpdatedAt
|
||||
};
|
||||
|
||||
private static CategoryDto MapCategoryToDto(KnowledgeBaseCategory c) => new()
|
||||
{
|
||||
Id = c.Id,
|
||||
Name = c.Name,
|
||||
Description = c.Description,
|
||||
SortOrder = c.SortOrder,
|
||||
ArticleCount = c.Articles?.Count ?? 0,
|
||||
CreatedAt = c.CreatedAt
|
||||
};
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
using HelpDesk.Data.Repositories;
|
||||
using HelpDesk.Models.DTOs;
|
||||
using HelpDesk.Models.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HelpDesk.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public class SLAController : ControllerBase
|
||||
{
|
||||
private readonly ISLARepository _slaRepository;
|
||||
|
||||
public SLAController(ISLARepository slaRepository)
|
||||
{
|
||||
_slaRepository = slaRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<SLADto>>> GetSLAPolicies()
|
||||
{
|
||||
var policies = await _slaRepository.GetAllAsync();
|
||||
return Ok(policies.Select(MapToDto));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<SLADto>> GetSLAPolicy(Guid id)
|
||||
{
|
||||
var policy = await _slaRepository.GetByIdAsync(id);
|
||||
if (policy == null) return NotFound(new { error = "SLA policy not found." });
|
||||
return Ok(MapToDto(policy));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<SLADto>> CreateSLAPolicy([FromBody] CreateSLARequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
return BadRequest(new { error = "Name is required." });
|
||||
if (request.ResponseTimeHours <= 0 || request.ResolutionTimeHours <= 0)
|
||||
return BadRequest(new { error = "Response and resolution times must be positive." });
|
||||
|
||||
var policy = new SLAPolicy
|
||||
{
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
ResponseTimeHours = request.ResponseTimeHours,
|
||||
ResolutionTimeHours = request.ResolutionTimeHours,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var created = await _slaRepository.CreateAsync(policy);
|
||||
return CreatedAtAction(nameof(GetSLAPolicy), new { id = created.Id }, MapToDto(created));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<SLADto>> UpdateSLAPolicy(Guid id, [FromBody] UpdateSLARequest request)
|
||||
{
|
||||
var policy = await _slaRepository.GetByIdAsync(id);
|
||||
if (policy == null) return NotFound(new { error = "SLA policy not found." });
|
||||
|
||||
if (request.Name != null) policy.Name = request.Name;
|
||||
if (request.Description != null) policy.Description = request.Description;
|
||||
if (request.ResponseTimeHours.HasValue) policy.ResponseTimeHours = request.ResponseTimeHours.Value;
|
||||
if (request.ResolutionTimeHours.HasValue) policy.ResolutionTimeHours = request.ResolutionTimeHours.Value;
|
||||
if (request.IsActive.HasValue) policy.IsActive = request.IsActive.Value;
|
||||
|
||||
var updated = await _slaRepository.UpdateAsync(policy);
|
||||
if (updated == null) return StatusCode(500, new { error = "Failed to update SLA policy." });
|
||||
|
||||
return Ok(MapToDto(updated));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteSLAPolicy(Guid id)
|
||||
{
|
||||
var deleted = await _slaRepository.DeleteAsync(id);
|
||||
if (!deleted) return NotFound(new { error = "SLA policy not found." });
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static SLADto MapToDto(SLAPolicy s) => new()
|
||||
{
|
||||
Id = s.Id,
|
||||
Name = s.Name,
|
||||
Description = s.Description,
|
||||
ResponseTimeHours = s.ResponseTimeHours,
|
||||
ResolutionTimeHours = s.ResolutionTimeHours,
|
||||
IsActive = s.IsActive,
|
||||
CreatedAt = s.CreatedAt
|
||||
};
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
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 UsersController : ControllerBase
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public UsersController(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers()
|
||||
{
|
||||
var users = await _userRepository.GetAllAsync();
|
||||
return Ok(users.Select(MapToDto));
|
||||
}
|
||||
|
||||
[HttpGet("agents")]
|
||||
public async Task<ActionResult<IEnumerable<UserDto>>> GetAgents()
|
||||
{
|
||||
var agents = await _userRepository.GetByRoleAsync(UserRole.Agent);
|
||||
return Ok(agents.Select(MapToDto));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<UserDto>> GetUser(Guid id)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(id);
|
||||
if (user == null) return NotFound(new { error = "User not found." });
|
||||
return Ok(MapToDto(user));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<UserDto>> UpdateUser(Guid id, [FromBody] UpdateUserRequest request)
|
||||
{
|
||||
var currentUserId = GetUserId();
|
||||
if (currentUserId == null) return Unauthorized();
|
||||
|
||||
var currentUser = await _userRepository.GetByIdAsync(currentUserId.Value);
|
||||
if (currentUser == null) return Unauthorized();
|
||||
|
||||
// Users can update themselves; Admins/SuperAdmins can update others
|
||||
if (currentUserId.Value != id && currentUser.Role < UserRole.Admin)
|
||||
return Forbid();
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(id);
|
||||
if (user == null) return NotFound(new { error = "User not found." });
|
||||
|
||||
if (request.FirstName != null) user.FirstName = request.FirstName;
|
||||
if (request.LastName != null) user.LastName = request.LastName;
|
||||
if (request.Email != null) user.Email = request.Email;
|
||||
if (request.PhoneNumber != null) user.PhoneNumber = request.PhoneNumber;
|
||||
if (request.AvatarUrl != null) user.AvatarUrl = request.AvatarUrl;
|
||||
|
||||
// Only admins can change role and active status
|
||||
if (currentUser.Role >= UserRole.Admin)
|
||||
{
|
||||
if (request.Role.HasValue) user.Role = request.Role.Value;
|
||||
if (request.IsActive.HasValue) user.IsActive = request.IsActive.Value;
|
||||
}
|
||||
|
||||
var updated = await _userRepository.UpdateAsync(user);
|
||||
if (updated == null) return StatusCode(500, new { error = "Failed to update user." });
|
||||
|
||||
return Ok(MapToDto(updated));
|
||||
}
|
||||
|
||||
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 UserDto MapToDto(User u) => new()
|
||||
{
|
||||
Id = u.Id,
|
||||
Email = u.Email,
|
||||
FirstName = u.FirstName,
|
||||
LastName = u.LastName,
|
||||
Role = u.Role,
|
||||
IsActive = u.IsActive,
|
||||
CreatedAt = u.CreatedAt,
|
||||
PhoneNumber = u.PhoneNumber,
|
||||
AvatarUrl = u.AvatarUrl
|
||||
};
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowMissingPrunePackageData>true</AllowMissingPrunePackageData>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HelpDesk.Models\HelpDesk.Models.csproj" />
|
||||
<ProjectReference Include="..\HelpDesk.Data\HelpDesk.Data.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,6 +0,0 @@
|
||||
@HelpDesk.API_HostAddress = http://localhost:5098
|
||||
|
||||
GET {{HelpDesk.API_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -1,56 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace HelpDesk.API.Middleware;
|
||||
|
||||
public class ExceptionMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ExceptionMiddleware> _logger;
|
||||
|
||||
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An unhandled exception occurred: {Message}", ex.Message);
|
||||
await HandleExceptionAsync(context, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
|
||||
var (statusCode, message) = exception switch
|
||||
{
|
||||
ArgumentException argEx => (HttpStatusCode.BadRequest, argEx.Message),
|
||||
KeyNotFoundException keyEx => (HttpStatusCode.NotFound, keyEx.Message),
|
||||
UnauthorizedAccessException => (HttpStatusCode.Unauthorized, "Unauthorized access."),
|
||||
InvalidOperationException opEx => (HttpStatusCode.BadRequest, opEx.Message),
|
||||
_ => (HttpStatusCode.InternalServerError, "An unexpected error occurred.")
|
||||
};
|
||||
|
||||
context.Response.StatusCode = (int)statusCode;
|
||||
|
||||
var response = new
|
||||
{
|
||||
error = message,
|
||||
statusCode = (int)statusCode
|
||||
};
|
||||
|
||||
await context.Response.WriteAsync(JsonSerializer.Serialize(response, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
using System.Text;
|
||||
using HelpDesk.API.Middleware;
|
||||
using HelpDesk.API.Services;
|
||||
using HelpDesk.Data;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Controllers
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// Database
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
||||
?? Environment.GetEnvironmentVariable("DB_CONNECTION")
|
||||
?? "Host=localhost;Port=5432;Database=helpdesk;Username=postgres;Password=postgres";
|
||||
|
||||
builder.Services.AddDbContext<HelpDeskDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
// Repositories
|
||||
builder.Services.AddRepositories();
|
||||
|
||||
// Services
|
||||
builder.Services.AddScoped<IJwtService, JwtService>();
|
||||
builder.Services.AddScoped<IPasswordService, PasswordService>();
|
||||
|
||||
// Authentication
|
||||
var jwtSecretKey = builder.Configuration["JwtSettings:SecretKey"] ?? "YourSuperSecretKeyHereThatShouldBeAtLeast32CharactersLong!!";
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = builder.Configuration["JwtSettings:Issuer"] ?? "HelpDesk",
|
||||
ValidAudience = builder.Configuration["JwtSettings:Audience"] ?? "HelpDeskAPI",
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecretKey)),
|
||||
ClockSkew = TimeSpan.Zero
|
||||
};
|
||||
});
|
||||
|
||||
// Authorization Policies
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("SuperAdmin", policy => policy.RequireRole("SuperAdmin"));
|
||||
options.AddPolicy("Admin", policy => policy.RequireRole("SuperAdmin", "Admin"));
|
||||
options.AddPolicy("Agent", policy => policy.RequireRole("SuperAdmin", "Admin", "Agent"));
|
||||
options.AddPolicy("Customer", policy => policy.RequireRole("SuperAdmin", "Admin", "Agent", "Customer"));
|
||||
});
|
||||
|
||||
// CORS
|
||||
var frontendUrl = builder.Configuration["FrontendUrl"] ?? "http://localhost:3000";
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("Frontend", policy =>
|
||||
{
|
||||
policy.WithOrigins(frontendUrl)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
// Swagger
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Title = "HelpDesk API",
|
||||
Version = "v1",
|
||||
Description = "HelpDesk API for ticket management"
|
||||
});
|
||||
|
||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "Bearer",
|
||||
BearerFormat = "JWT",
|
||||
In = ParameterLocation.Header,
|
||||
Description = "Enter your JWT token"
|
||||
});
|
||||
|
||||
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
}
|
||||
},
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Exception Middleware
|
||||
app.UseMiddleware<ExceptionMiddleware>();
|
||||
|
||||
// Swagger
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
// CORS
|
||||
app.UseCors("Frontend");
|
||||
|
||||
// Auth
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// Map Controllers
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5098",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using HelpDesk.Models.Enums;
|
||||
|
||||
namespace HelpDesk.API.Services;
|
||||
|
||||
public interface IJwtService
|
||||
{
|
||||
string GenerateToken(Guid userId, string email, UserRole role);
|
||||
ClaimsPrincipal? ValidateToken(string token);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace HelpDesk.API.Services;
|
||||
|
||||
public interface IPasswordService
|
||||
{
|
||||
string HashPassword(string password);
|
||||
bool VerifyPassword(string password, string hashedPassword);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using HelpDesk.Models.Enums;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace HelpDesk.API.Services;
|
||||
|
||||
public class JwtService : IJwtService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public JwtService(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public string GenerateToken(Guid userId, string email, UserRole role)
|
||||
{
|
||||
var secretKey = _configuration["JwtSettings:SecretKey"] ?? throw new InvalidOperationException("JWT SecretKey is not configured.");
|
||||
var issuer = _configuration["JwtSettings:Issuer"] ?? "HelpDesk";
|
||||
var audience = _configuration["JwtSettings:Audience"] ?? "HelpDeskAPI";
|
||||
var expiryMinutes = int.TryParse(_configuration["JwtSettings:ExpiryMinutes"], out var minutes) ? minutes : 60;
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Email, email),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
new Claim(ClaimTypes.Role, role.ToString()),
|
||||
new Claim("userId", userId.ToString())
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: issuer,
|
||||
audience: audience,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddMinutes(expiryMinutes),
|
||||
signingCredentials: credentials
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
public ClaimsPrincipal? ValidateToken(string token)
|
||||
{
|
||||
var secretKey = _configuration["JwtSettings:SecretKey"] ?? throw new InvalidOperationException("JWT SecretKey is not configured.");
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.UTF8.GetBytes(secretKey);
|
||||
|
||||
try
|
||||
{
|
||||
var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = _configuration["JwtSettings:Issuer"],
|
||||
ValidAudience = _configuration["JwtSettings:Audience"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey(key),
|
||||
ClockSkew = TimeSpan.Zero
|
||||
}, out _);
|
||||
|
||||
return principal;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace HelpDesk.API.Services;
|
||||
|
||||
public class PasswordService : IPasswordService
|
||||
{
|
||||
public string HashPassword(string password)
|
||||
{
|
||||
return BCrypt.Net.BCrypt.HashPassword(password);
|
||||
}
|
||||
|
||||
public bool VerifyPassword(string password, string hashedPassword)
|
||||
{
|
||||
return BCrypt.Net.BCrypt.Verify(password, hashedPassword);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=helpdesk;Username=postgres;Password=postgres"
|
||||
},
|
||||
"JwtSettings": {
|
||||
"SecretKey": "DevSuperSecretKeyForDevelopmentOnly_32Chars!!",
|
||||
"Issuer": "HelpDesk",
|
||||
"Audience": "HelpDeskAPI",
|
||||
"ExpiryMinutes": 120
|
||||
},
|
||||
"FrontendUrl": "http://localhost:3000"
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=helpdesk;Username=postgres;Password=postgres"
|
||||
},
|
||||
"JwtSettings": {
|
||||
"SecretKey": "YourSuperSecretKeyHereThatShouldBeAtLeast32CharactersLong!!",
|
||||
"Issuer": "HelpDesk",
|
||||
"Audience": "HelpDeskAPI",
|
||||
"ExpiryMinutes": 60
|
||||
},
|
||||
"FrontendUrl": "http://localhost:3000"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using HelpDesk.Data.Repositories;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HelpDesk.Data;
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddRepositories(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
services.AddScoped<ITicketRepository, TicketRepository>();
|
||||
services.AddScoped<ITicketCommentRepository, TicketCommentRepository>();
|
||||
services.AddScoped<IGroupRepository, GroupRepository>();
|
||||
services.AddScoped<ISLARepository, SLARepository>();
|
||||
services.AddScoped<IKnowledgeBaseRepository, KnowledgeBaseRepository>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HelpDesk.Models\HelpDesk.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,306 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using HelpDesk.Models.Entities;
|
||||
|
||||
namespace HelpDesk.Data;
|
||||
|
||||
public class HelpDeskDbContext : DbContext
|
||||
{
|
||||
public HelpDeskDbContext(DbContextOptions<HelpDeskDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<Ticket> Tickets => Set<Ticket>();
|
||||
public DbSet<TicketComment> TicketComments => Set<TicketComment>();
|
||||
public DbSet<Group> Groups => Set<Group>();
|
||||
public DbSet<SLAPolicy> SLAPolicies => Set<SLAPolicy>();
|
||||
public DbSet<KnowledgeBaseArticle> KnowledgeBaseArticles => Set<KnowledgeBaseArticle>();
|
||||
public DbSet<KnowledgeBaseCategory> KnowledgeBaseCategories => Set<KnowledgeBaseCategory>();
|
||||
public DbSet<ActivityLog> ActivityLogs => Set<ActivityLog>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// ── User ──────────────────────────────────────────────
|
||||
modelBuilder.Entity<User>(entity =>
|
||||
{
|
||||
entity.ToTable("users");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.FirstName).HasColumnName("first_name").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.LastName).HasColumnName("last_name").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.Email).HasColumnName("email").HasMaxLength(256).IsRequired();
|
||||
entity.Property(e => e.PasswordHash).HasColumnName("password_hash").HasMaxLength(500).IsRequired();
|
||||
entity.Property(e => e.Role).HasColumnName("role").HasMaxLength(50).IsRequired();
|
||||
entity.Property(e => e.IsActive).HasColumnName("is_active");
|
||||
entity.Property(e => e.PhoneNumber).HasColumnName("phone_number").HasMaxLength(50);
|
||||
entity.Property(e => e.AvatarUrl).HasColumnName("avatar_url").HasMaxLength(500);
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
|
||||
entity.HasIndex(e => e.Email).IsUnique().HasDatabaseName("ix_users_email");
|
||||
});
|
||||
|
||||
// ── Ticket ────────────────────────────────────────────
|
||||
modelBuilder.Entity<Ticket>(entity =>
|
||||
{
|
||||
entity.ToTable("tickets");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.Title).HasColumnName("title").HasMaxLength(200).IsRequired();
|
||||
entity.Property(e => e.Description).HasColumnName("description").IsRequired();
|
||||
entity.Property(e => e.Status).HasColumnName("status").HasMaxLength(50).IsRequired();
|
||||
entity.Property(e => e.Priority).HasColumnName("priority").HasMaxLength(50).IsRequired();
|
||||
entity.Property(e => e.TicketNumber).HasColumnName("ticket_number").HasMaxLength(50).IsRequired();
|
||||
entity.Property(e => e.RequesterId).HasColumnName("requester_id");
|
||||
entity.Property(e => e.AssigneeId).HasColumnName("assignee_id");
|
||||
entity.Property(e => e.GroupId).HasColumnName("group_id");
|
||||
entity.Property(e => e.SlaPolicyId).HasColumnName("sla_policy_id");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
entity.Property(e => e.ResolvedAt).HasColumnName("resolved_at");
|
||||
entity.Property(e => e.ClosedAt).HasColumnName("closed_at");
|
||||
|
||||
entity.HasIndex(e => e.TicketNumber).IsUnique().HasDatabaseName("ix_tickets_ticket_number");
|
||||
|
||||
// Cascade delete: Ticket -> TicketComment
|
||||
entity.HasMany(e => e.Comments)
|
||||
.WithOne(c => c.Ticket)
|
||||
.HasForeignKey(c => c.TicketId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Restrict: User FK references (Requester)
|
||||
entity.HasOne(e => e.Requester)
|
||||
.WithMany(u => u.RequestedTickets)
|
||||
.HasForeignKey(e => e.RequesterId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// Restrict: User FK references (Assignee)
|
||||
entity.HasOne(e => e.Assignee)
|
||||
.WithMany(u => u.AssignedTickets)
|
||||
.HasForeignKey(e => e.AssigneeId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
entity.HasOne(e => e.Group)
|
||||
.WithMany(g => g.Tickets)
|
||||
.HasForeignKey(e => e.GroupId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
entity.HasOne(e => e.SlaPolicy)
|
||||
.WithMany(s => s.Tickets)
|
||||
.HasForeignKey(e => e.SlaPolicyId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
// ── TicketComment ─────────────────────────────────────
|
||||
modelBuilder.Entity<TicketComment>(entity =>
|
||||
{
|
||||
entity.ToTable("ticket_comments");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.TicketId).HasColumnName("ticket_id");
|
||||
entity.Property(e => e.UserId).HasColumnName("user_id");
|
||||
entity.Property(e => e.Content).HasColumnName("content").IsRequired();
|
||||
entity.Property(e => e.IsInternal).HasColumnName("is_internal");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
|
||||
// Restrict: User FK reference
|
||||
entity.HasOne(e => e.User)
|
||||
.WithMany(u => u.Comments)
|
||||
.HasForeignKey(e => e.UserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
// ── Group ─────────────────────────────────────────────
|
||||
modelBuilder.Entity<Group>(entity =>
|
||||
{
|
||||
entity.ToTable("groups");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.Name).HasColumnName("name").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.Description).HasColumnName("description").HasMaxLength(500);
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
});
|
||||
|
||||
// ── SLAPolicy ─────────────────────────────────────────
|
||||
modelBuilder.Entity<SLAPolicy>(entity =>
|
||||
{
|
||||
entity.ToTable("sla_policies");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.Name).HasColumnName("name").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.Description).HasColumnName("description").HasMaxLength(500);
|
||||
entity.Property(e => e.ResponseTimeHours).HasColumnName("response_time_hours");
|
||||
entity.Property(e => e.ResolutionTimeHours).HasColumnName("resolution_time_hours");
|
||||
entity.Property(e => e.IsActive).HasColumnName("is_active");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
});
|
||||
|
||||
// ── KnowledgeBaseArticle ──────────────────────────────
|
||||
modelBuilder.Entity<KnowledgeBaseArticle>(entity =>
|
||||
{
|
||||
entity.ToTable("knowledge_base_articles");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.Title).HasColumnName("title").HasMaxLength(200).IsRequired();
|
||||
entity.Property(e => e.Content).HasColumnName("content").IsRequired();
|
||||
entity.Property(e => e.CategoryId).HasColumnName("category_id");
|
||||
entity.Property(e => e.AuthorId).HasColumnName("author_id");
|
||||
entity.Property(e => e.IsPublished).HasColumnName("is_published");
|
||||
entity.Property(e => e.ViewCount).HasColumnName("view_count");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
|
||||
entity.HasOne(e => e.Category)
|
||||
.WithMany(c => c.Articles)
|
||||
.HasForeignKey(e => e.CategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
entity.HasOne(e => e.Author)
|
||||
.WithMany(u => u.Articles)
|
||||
.HasForeignKey(e => e.AuthorId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
// ── KnowledgeBaseCategory ─────────────────────────────
|
||||
modelBuilder.Entity<KnowledgeBaseCategory>(entity =>
|
||||
{
|
||||
entity.ToTable("knowledge_base_categories");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.Name).HasColumnName("name").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.Description).HasColumnName("description").HasMaxLength(500);
|
||||
entity.Property(e => e.SortOrder).HasColumnName("sort_order");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
});
|
||||
|
||||
// ── ActivityLog ───────────────────────────────────────
|
||||
modelBuilder.Entity<ActivityLog>(entity =>
|
||||
{
|
||||
entity.ToTable("activity_logs");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.UserId).HasColumnName("user_id");
|
||||
entity.Property(e => e.EntityType).HasColumnName("entity_type").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.EntityId).HasColumnName("entity_id");
|
||||
entity.Property(e => e.Action).HasColumnName("action").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.Details).HasColumnName("details");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
|
||||
entity.HasOne(e => e.User)
|
||||
.WithMany(u => u.ActivityLogs)
|
||||
.HasForeignKey(e => e.UserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
// ── Seed Data ─────────────────────────────────────────
|
||||
SeedData(modelBuilder);
|
||||
}
|
||||
|
||||
private static void SeedData(ModelBuilder modelBuilder)
|
||||
{
|
||||
var adminId = Guid.Parse("a0000000-0000-0000-0000-000000000001");
|
||||
var generalGroupId = Guid.Parse("b0000000-0000-0000-0000-000000000001");
|
||||
var ventasGroupId = Guid.Parse("b0000000-0000-0000-0000-000000000002");
|
||||
var desarrolloGroupId = Guid.Parse("b0000000-0000-0000-0000-000000000003");
|
||||
var standardSlaId = Guid.Parse("c0000000-0000-0000-0000-000000000001");
|
||||
var premiumSlaId = Guid.Parse("c0000000-0000-0000-0000-000000000002");
|
||||
var generalCategoryId = Guid.Parse("d0000000-0000-0000-0000-000000000001");
|
||||
var faqCategoryId = Guid.Parse("d0000000-0000-0000-0000-000000000002");
|
||||
var tutorialsCategoryId = Guid.Parse("d0000000-0000-0000-0000-000000000003");
|
||||
|
||||
// SuperAdmin user
|
||||
modelBuilder.Entity<User>().HasData(new User
|
||||
{
|
||||
Id = adminId,
|
||||
FirstName = "Super",
|
||||
LastName = "Admin",
|
||||
Email = "admin@helpdesk.local",
|
||||
// BCrypt hash for "Admin123!"
|
||||
PasswordHash = "$2a$11$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy",
|
||||
Role = HelpDesk.Models.Enums.UserRole.SuperAdmin,
|
||||
IsActive = true,
|
||||
PhoneNumber = "+1-555-0100",
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
UpdatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
});
|
||||
|
||||
// Default groups
|
||||
modelBuilder.Entity<Group>().HasData(
|
||||
new Group
|
||||
{
|
||||
Id = generalGroupId,
|
||||
Name = "Soporte General",
|
||||
Description = "Grupo de soporte técnico general",
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
},
|
||||
new Group
|
||||
{
|
||||
Id = ventasGroupId,
|
||||
Name = "Ventas",
|
||||
Description = "Grupo de soporte de ventas",
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
},
|
||||
new Group
|
||||
{
|
||||
Id = desarrolloGroupId,
|
||||
Name = "Desarrollo",
|
||||
Description = "Grupo de desarrollo de software",
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
}
|
||||
);
|
||||
|
||||
// SLA policies
|
||||
modelBuilder.Entity<SLAPolicy>().HasData(
|
||||
new SLAPolicy
|
||||
{
|
||||
Id = standardSlaId,
|
||||
Name = "Estándar",
|
||||
Description = "Política estándar: respuesta en 4 horas, resolución en 24 horas",
|
||||
ResponseTimeHours = 4,
|
||||
ResolutionTimeHours = 24,
|
||||
IsActive = true,
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
},
|
||||
new SLAPolicy
|
||||
{
|
||||
Id = premiumSlaId,
|
||||
Name = "Premium",
|
||||
Description = "Política premium: respuesta en 1 hora, resolución en 8 horas",
|
||||
ResponseTimeHours = 1,
|
||||
ResolutionTimeHours = 8,
|
||||
IsActive = true,
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
}
|
||||
);
|
||||
|
||||
// Knowledge base categories
|
||||
modelBuilder.Entity<KnowledgeBaseCategory>().HasData(
|
||||
new KnowledgeBaseCategory
|
||||
{
|
||||
Id = generalCategoryId,
|
||||
Name = "General",
|
||||
Description = "Artículos generales de ayuda",
|
||||
SortOrder = 1,
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
},
|
||||
new KnowledgeBaseCategory
|
||||
{
|
||||
Id = faqCategoryId,
|
||||
Name = "Preguntas Frecuentes",
|
||||
Description = "Preguntas frecuentes y respuestas",
|
||||
SortOrder = 2,
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
},
|
||||
new KnowledgeBaseCategory
|
||||
{
|
||||
Id = tutorialsCategoryId,
|
||||
Name = "Tutoriales",
|
||||
Description = "Guías y tutoriales paso a paso",
|
||||
SortOrder = 3,
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public interface IKnowledgeBaseRepository
|
||||
{
|
||||
// Articles
|
||||
Task<KnowledgeBaseArticle?> GetArticleByIdAsync(Guid id);
|
||||
Task<IEnumerable<KnowledgeBaseArticle>> GetAllArticlesAsync();
|
||||
Task<IEnumerable<KnowledgeBaseArticle>> GetPublishedArticlesAsync();
|
||||
Task<IEnumerable<KnowledgeBaseArticle>> SearchArticlesAsync(string query);
|
||||
Task<KnowledgeBaseArticle> CreateArticleAsync(KnowledgeBaseArticle article);
|
||||
Task<KnowledgeBaseArticle?> UpdateArticleAsync(KnowledgeBaseArticle article);
|
||||
Task<bool> DeleteArticleAsync(Guid id);
|
||||
|
||||
// Categories
|
||||
Task<KnowledgeBaseCategory?> GetCategoryByIdAsync(Guid id);
|
||||
Task<IEnumerable<KnowledgeBaseCategory>> GetAllCategoriesAsync();
|
||||
Task<KnowledgeBaseCategory> CreateCategoryAsync(KnowledgeBaseCategory category);
|
||||
Task<KnowledgeBaseCategory?> UpdateCategoryAsync(KnowledgeBaseCategory category);
|
||||
Task<bool> DeleteCategoryAsync(Guid id);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public interface IRepository<T> where T : class
|
||||
{
|
||||
Task<T?> GetByIdAsync(Guid id);
|
||||
Task<IEnumerable<T>> GetAllAsync();
|
||||
Task<T> AddAsync(T entity);
|
||||
Task UpdateAsync(T entity);
|
||||
Task DeleteAsync(T entity);
|
||||
Task<int> SaveChangesAsync();
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
using HelpDesk.Models.Enums;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public interface ITicketRepository
|
||||
{
|
||||
Task<Ticket?> GetByIdAsync(Guid id);
|
||||
Task<IEnumerable<Ticket>> GetAllAsync(TicketStatus? status = null, TicketPriority? priority = null, Guid? assigneeId = null, Guid? requesterId = null, int page = 1, int pageSize = 20);
|
||||
Task<int> GetCountAsync(TicketStatus? status = null, TicketPriority? priority = null, Guid? assigneeId = null, Guid? requesterId = null);
|
||||
Task<Ticket> CreateAsync(Ticket ticket);
|
||||
Task<Ticket?> UpdateAsync(Ticket ticket);
|
||||
Task<bool> DeleteAsync(Guid id);
|
||||
Task<int> GetTotalCountAsync();
|
||||
Task<int> GetCountByStatusAsync(TicketStatus status);
|
||||
Task<int> GetCountByPriorityAsync(TicketPriority priority);
|
||||
Task<double> GetAverageResponseTimeAsync();
|
||||
Task<double> GetAverageResolutionTimeAsync();
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
using HelpDesk.Models.Enums;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public interface IUserRepository
|
||||
{
|
||||
Task<User?> GetByIdAsync(Guid id);
|
||||
Task<User?> GetByEmailAsync(string email);
|
||||
Task<IEnumerable<User>> GetAllAsync();
|
||||
Task<IEnumerable<User>> GetByRoleAsync(UserRole role);
|
||||
Task<User> CreateAsync(User user);
|
||||
Task<User?> UpdateAsync(User user);
|
||||
Task<bool> ExistsAsync(string email);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public class KnowledgeBaseRepository : IKnowledgeBaseRepository
|
||||
{
|
||||
private readonly HelpDeskDbContext _context;
|
||||
|
||||
public KnowledgeBaseRepository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// Articles
|
||||
public async Task<KnowledgeBaseArticle?> GetArticleByIdAsync(Guid id)
|
||||
=> await _context.KnowledgeBaseArticles
|
||||
.Include(a => a.Category)
|
||||
.Include(a => a.Author)
|
||||
.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
||||
public async Task<IEnumerable<KnowledgeBaseArticle>> GetAllArticlesAsync()
|
||||
=> await _context.KnowledgeBaseArticles
|
||||
.Include(a => a.Category)
|
||||
.Include(a => a.Author)
|
||||
.OrderByDescending(a => a.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<IEnumerable<KnowledgeBaseArticle>> GetPublishedArticlesAsync()
|
||||
=> await _context.KnowledgeBaseArticles
|
||||
.Include(a => a.Category)
|
||||
.Include(a => a.Author)
|
||||
.Where(a => a.IsPublished)
|
||||
.OrderByDescending(a => a.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<IEnumerable<KnowledgeBaseArticle>> SearchArticlesAsync(string query)
|
||||
{
|
||||
var lowerQuery = query.ToLower();
|
||||
return await _context.KnowledgeBaseArticles
|
||||
.Include(a => a.Category)
|
||||
.Include(a => a.Author)
|
||||
.Where(a => a.IsPublished && (a.Title.ToLower().Contains(lowerQuery) || a.Content.ToLower().Contains(lowerQuery)))
|
||||
.OrderByDescending(a => a.ViewCount)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<KnowledgeBaseArticle> CreateArticleAsync(KnowledgeBaseArticle article)
|
||||
{
|
||||
_context.KnowledgeBaseArticles.Add(article);
|
||||
await _context.SaveChangesAsync();
|
||||
return article;
|
||||
}
|
||||
|
||||
public async Task<KnowledgeBaseArticle?> UpdateArticleAsync(KnowledgeBaseArticle article)
|
||||
{
|
||||
var existing = await _context.KnowledgeBaseArticles.FindAsync(article.Id);
|
||||
if (existing == null) return null;
|
||||
|
||||
existing.Title = article.Title;
|
||||
existing.Content = article.Content;
|
||||
existing.CategoryId = article.CategoryId;
|
||||
existing.IsPublished = article.IsPublished;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteArticleAsync(Guid id)
|
||||
{
|
||||
var article = await _context.KnowledgeBaseArticles.FindAsync(id);
|
||||
if (article == null) return false;
|
||||
_context.KnowledgeBaseArticles.Remove(article);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Categories
|
||||
public async Task<KnowledgeBaseCategory?> GetCategoryByIdAsync(Guid id)
|
||||
=> await _context.KnowledgeBaseCategories
|
||||
.Include(c => c.Articles)
|
||||
.FirstOrDefaultAsync(c => c.Id == id);
|
||||
|
||||
public async Task<IEnumerable<KnowledgeBaseCategory>> GetAllCategoriesAsync()
|
||||
=> await _context.KnowledgeBaseCategories
|
||||
.Include(c => c.Articles)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.ThenBy(c => c.Name)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<KnowledgeBaseCategory> CreateCategoryAsync(KnowledgeBaseCategory category)
|
||||
{
|
||||
_context.KnowledgeBaseCategories.Add(category);
|
||||
await _context.SaveChangesAsync();
|
||||
return category;
|
||||
}
|
||||
|
||||
public async Task<KnowledgeBaseCategory?> UpdateCategoryAsync(KnowledgeBaseCategory category)
|
||||
{
|
||||
var existing = await _context.KnowledgeBaseCategories.FindAsync(category.Id);
|
||||
if (existing == null) return null;
|
||||
|
||||
existing.Name = category.Name;
|
||||
existing.Description = category.Description;
|
||||
existing.SortOrder = category.SortOrder;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteCategoryAsync(Guid id)
|
||||
{
|
||||
var category = await _context.KnowledgeBaseCategories.FindAsync(id);
|
||||
if (category == null) return false;
|
||||
_context.KnowledgeBaseCategories.Remove(category);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public interface ITicketCommentRepository
|
||||
{
|
||||
Task<TicketComment?> GetByIdAsync(Guid id);
|
||||
Task<IEnumerable<TicketComment>> GetByTicketIdAsync(Guid ticketId);
|
||||
Task<TicketComment> CreateAsync(TicketComment comment);
|
||||
Task<bool> DeleteAsync(Guid id);
|
||||
}
|
||||
|
||||
public interface IGroupRepository
|
||||
{
|
||||
Task<Group?> GetByIdAsync(Guid id);
|
||||
Task<IEnumerable<Group>> GetAllAsync();
|
||||
Task<Group> CreateAsync(Group group);
|
||||
Task<Group?> UpdateAsync(Group group);
|
||||
Task<bool> DeleteAsync(Guid id);
|
||||
}
|
||||
|
||||
public interface ISLARepository
|
||||
{
|
||||
Task<SLAPolicy?> GetByIdAsync(Guid id);
|
||||
Task<IEnumerable<SLAPolicy>> GetAllAsync();
|
||||
Task<SLAPolicy> CreateAsync(SLAPolicy policy);
|
||||
Task<SLAPolicy?> UpdateAsync(SLAPolicy policy);
|
||||
Task<bool> DeleteAsync(Guid id);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public class Repository<T> : IRepository<T> where T : class
|
||||
{
|
||||
protected readonly HelpDeskDbContext _context;
|
||||
protected readonly DbSet<T> _dbSet;
|
||||
|
||||
public Repository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
_dbSet = context.Set<T>();
|
||||
}
|
||||
|
||||
public virtual async Task<T?> GetByIdAsync(Guid id)
|
||||
{
|
||||
return await _dbSet.FindAsync(id);
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<T>> GetAllAsync()
|
||||
{
|
||||
return await _dbSet.ToListAsync();
|
||||
}
|
||||
|
||||
public virtual async Task<T> AddAsync(T entity)
|
||||
{
|
||||
await _dbSet.AddAsync(entity);
|
||||
await SaveChangesAsync();
|
||||
return entity;
|
||||
}
|
||||
|
||||
public virtual async Task UpdateAsync(T entity)
|
||||
{
|
||||
_dbSet.Update(entity);
|
||||
await SaveChangesAsync();
|
||||
}
|
||||
|
||||
public virtual async Task DeleteAsync(T entity)
|
||||
{
|
||||
_dbSet.Remove(entity);
|
||||
await SaveChangesAsync();
|
||||
}
|
||||
|
||||
public virtual async Task<int> SaveChangesAsync()
|
||||
{
|
||||
return await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public class TicketCommentRepository : ITicketCommentRepository
|
||||
{
|
||||
private readonly HelpDeskDbContext _context;
|
||||
|
||||
public TicketCommentRepository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<TicketComment?> GetByIdAsync(Guid id)
|
||||
=> await _context.TicketComments.FindAsync(id);
|
||||
|
||||
public async Task<IEnumerable<TicketComment>> GetByTicketIdAsync(Guid ticketId)
|
||||
=> await _context.TicketComments
|
||||
.Include(c => c.User)
|
||||
.Where(c => c.TicketId == ticketId)
|
||||
.OrderBy(c => c.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<TicketComment> CreateAsync(TicketComment comment)
|
||||
{
|
||||
_context.TicketComments.Add(comment);
|
||||
await _context.SaveChangesAsync();
|
||||
return comment;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
var comment = await _context.TicketComments.FindAsync(id);
|
||||
if (comment == null) return false;
|
||||
_context.TicketComments.Remove(comment);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class GroupRepository : IGroupRepository
|
||||
{
|
||||
private readonly HelpDeskDbContext _context;
|
||||
|
||||
public GroupRepository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Group?> GetByIdAsync(Guid id)
|
||||
=> await _context.Groups.FindAsync(id);
|
||||
|
||||
public async Task<IEnumerable<Group>> GetAllAsync()
|
||||
=> await _context.Groups.OrderBy(g => g.Name).ToListAsync();
|
||||
|
||||
public async Task<Group> CreateAsync(Group group)
|
||||
{
|
||||
_context.Groups.Add(group);
|
||||
await _context.SaveChangesAsync();
|
||||
return group;
|
||||
}
|
||||
|
||||
public async Task<Group?> UpdateAsync(Group group)
|
||||
{
|
||||
var existing = await _context.Groups.FindAsync(group.Id);
|
||||
if (existing == null) return null;
|
||||
|
||||
existing.Name = group.Name;
|
||||
existing.Description = group.Description;
|
||||
existing.IsActive = group.IsActive;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
var group = await _context.Groups.FindAsync(id);
|
||||
if (group == null) return false;
|
||||
_context.Groups.Remove(group);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class SLARepository : ISLARepository
|
||||
{
|
||||
private readonly HelpDeskDbContext _context;
|
||||
|
||||
public SLARepository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<SLAPolicy?> GetByIdAsync(Guid id)
|
||||
=> await _context.SLAPolicies.FindAsync(id);
|
||||
|
||||
public async Task<IEnumerable<SLAPolicy>> GetAllAsync()
|
||||
=> await _context.SLAPolicies.OrderBy(s => s.Name).ToListAsync();
|
||||
|
||||
public async Task<SLAPolicy> CreateAsync(SLAPolicy policy)
|
||||
{
|
||||
_context.SLAPolicies.Add(policy);
|
||||
await _context.SaveChangesAsync();
|
||||
return policy;
|
||||
}
|
||||
|
||||
public async Task<SLAPolicy?> UpdateAsync(SLAPolicy policy)
|
||||
{
|
||||
var existing = await _context.SLAPolicies.FindAsync(policy.Id);
|
||||
if (existing == null) return null;
|
||||
|
||||
existing.Name = policy.Name;
|
||||
existing.Description = policy.Description;
|
||||
existing.ResponseTimeHours = policy.ResponseTimeHours;
|
||||
existing.ResolutionTimeHours = policy.ResolutionTimeHours;
|
||||
existing.IsActive = policy.IsActive;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
var policy = await _context.SLAPolicies.FindAsync(id);
|
||||
if (policy == null) return false;
|
||||
_context.SLAPolicies.Remove(policy);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
using HelpDesk.Models.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public class TicketRepository : ITicketRepository
|
||||
{
|
||||
private readonly HelpDeskDbContext _context;
|
||||
|
||||
public TicketRepository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
private IQueryable<Ticket> GetQueryable()
|
||||
=> _context.Tickets
|
||||
.Include(t => t.Requester)
|
||||
.Include(t => t.Assignee)
|
||||
.Include(t => t.Group)
|
||||
.Include(t => t.SlaPolicy)
|
||||
.Include(t => t.Comments);
|
||||
|
||||
public async Task<Ticket?> GetByIdAsync(Guid id)
|
||||
=> await GetQueryable().FirstOrDefaultAsync(t => t.Id == id);
|
||||
|
||||
public async Task<IEnumerable<Ticket>> GetAllAsync(TicketStatus? status, TicketPriority? priority, Guid? assigneeId, Guid? requesterId, int page = 1, int pageSize = 20)
|
||||
{
|
||||
var query = GetQueryable().AsQueryable();
|
||||
|
||||
if (status.HasValue) query = query.Where(t => t.Status == status.Value);
|
||||
if (priority.HasValue) query = query.Where(t => t.Priority == priority.Value);
|
||||
if (assigneeId.HasValue) query = query.Where(t => t.AssigneeId == assigneeId.Value);
|
||||
if (requesterId.HasValue) query = query.Where(t => t.RequesterId == requesterId.Value);
|
||||
|
||||
return await query.OrderByDescending(t => t.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int> GetCountAsync(TicketStatus? status, TicketPriority? priority, Guid? assigneeId, Guid? requesterId)
|
||||
{
|
||||
var query = _context.Tickets.AsQueryable();
|
||||
|
||||
if (status.HasValue) query = query.Where(t => t.Status == status.Value);
|
||||
if (priority.HasValue) query = query.Where(t => t.Priority == priority.Value);
|
||||
if (assigneeId.HasValue) query = query.Where(t => t.AssigneeId == assigneeId.Value);
|
||||
if (requesterId.HasValue) query = query.Where(t => t.RequesterId == requesterId.Value);
|
||||
|
||||
return await query.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<Ticket> CreateAsync(Ticket ticket)
|
||||
{
|
||||
_context.Tickets.Add(ticket);
|
||||
await _context.SaveChangesAsync();
|
||||
return ticket;
|
||||
}
|
||||
|
||||
public async Task<Ticket?> UpdateAsync(Ticket ticket)
|
||||
{
|
||||
var existing = await _context.Tickets.FindAsync(ticket.Id);
|
||||
if (existing == null) return null;
|
||||
|
||||
existing.Title = ticket.Title;
|
||||
existing.Description = ticket.Description;
|
||||
existing.Priority = ticket.Priority;
|
||||
existing.Status = ticket.Status;
|
||||
existing.AssigneeId = ticket.AssigneeId;
|
||||
existing.GroupId = ticket.GroupId;
|
||||
existing.SlaPolicyId = ticket.SlaPolicyId;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
if (ticket.Status == TicketStatus.Resolved && existing.ResolvedAt == null)
|
||||
existing.ResolvedAt = DateTime.UtcNow;
|
||||
if (ticket.Status == TicketStatus.Closed && existing.ClosedAt == null)
|
||||
existing.ClosedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
var ticket = await _context.Tickets.FindAsync(id);
|
||||
if (ticket == null) return false;
|
||||
_context.Tickets.Remove(ticket);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalCountAsync()
|
||||
=> await _context.Tickets.CountAsync();
|
||||
|
||||
public async Task<int> GetCountByStatusAsync(TicketStatus status)
|
||||
=> await _context.Tickets.CountAsync(t => t.Status == status);
|
||||
|
||||
public async Task<int> GetCountByPriorityAsync(TicketPriority priority)
|
||||
=> await _context.Tickets.CountAsync(t => t.Priority == priority);
|
||||
|
||||
public async Task<double> GetAverageResponseTimeAsync()
|
||||
{
|
||||
var tickets = await _context.Tickets
|
||||
.Where(t => t.Comments.Any())
|
||||
.Select(t => new
|
||||
{
|
||||
t.CreatedAt,
|
||||
FirstCommentAt = t.Comments.OrderBy(c => c.CreatedAt).Select(c => c.CreatedAt).FirstOrDefault()
|
||||
})
|
||||
.Where(t => t.FirstCommentAt != default(DateTime))
|
||||
.ToListAsync();
|
||||
|
||||
if (!tickets.Any()) return 0;
|
||||
return tickets.Average(t => (t.FirstCommentAt - t.CreatedAt).TotalMinutes);
|
||||
}
|
||||
|
||||
public async Task<double> GetAverageResolutionTimeAsync()
|
||||
{
|
||||
var tickets = await _context.Tickets
|
||||
.Where(t => t.ResolvedAt.HasValue)
|
||||
.Select(t => new { t.CreatedAt, t.ResolvedAt })
|
||||
.ToListAsync();
|
||||
|
||||
if (!tickets.Any()) return 0;
|
||||
return tickets.Average(t => (t.ResolvedAt!.Value - t.CreatedAt).TotalMinutes);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
using HelpDesk.Models.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
{
|
||||
private readonly HelpDeskDbContext _context;
|
||||
|
||||
public UserRepository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<User?> GetByIdAsync(Guid id)
|
||||
=> await _context.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||||
|
||||
public async Task<User?> GetByEmailAsync(string email)
|
||||
=> await _context.Users.FirstOrDefaultAsync(u => u.Email == email);
|
||||
|
||||
public async Task<IEnumerable<User>> GetAllAsync()
|
||||
=> await _context.Users.OrderBy(u => u.LastName).ToListAsync();
|
||||
|
||||
public async Task<IEnumerable<User>> GetByRoleAsync(UserRole role)
|
||||
=> await _context.Users.Where(u => u.Role == role && u.IsActive).OrderBy(u => u.LastName).ToListAsync();
|
||||
|
||||
public async Task<User> CreateAsync(User user)
|
||||
{
|
||||
_context.Users.Add(user);
|
||||
await _context.SaveChangesAsync();
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<User?> UpdateAsync(User user)
|
||||
{
|
||||
var existing = await _context.Users.FindAsync(user.Id);
|
||||
if (existing == null) return null;
|
||||
|
||||
existing.FirstName = user.FirstName;
|
||||
existing.LastName = user.LastName;
|
||||
existing.Email = user.Email;
|
||||
existing.PhoneNumber = user.PhoneNumber;
|
||||
existing.AvatarUrl = user.AvatarUrl;
|
||||
existing.Role = user.Role;
|
||||
existing.IsActive = user.IsActive;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string email)
|
||||
=> await _context.Users.AnyAsync(u => u.Email == email);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using HelpDesk.Models.Enums;
|
||||
|
||||
namespace HelpDesk.Models.DTOs;
|
||||
|
||||
public class LoginRequest
|
||||
{
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class RegisterRequest
|
||||
{
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AuthResponse
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public DateTime Expiration { get; set; }
|
||||
public UserDto User { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class UserDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
public UserRole Role { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public string? PhoneNumber { get; set; }
|
||||
public string? AvatarUrl { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateUserRequest
|
||||
{
|
||||
public string? FirstName { get; set; }
|
||||
public string? LastName { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? PhoneNumber { get; set; }
|
||||
public string? AvatarUrl { get; set; }
|
||||
public UserRole? Role { get; set; }
|
||||
public bool? IsActive { get; set; }
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using HelpDesk.Models.Enums;
|
||||
|
||||
namespace HelpDesk.Models.DTOs;
|
||||
|
||||
public class DashboardStatsDto
|
||||
{
|
||||
public int TotalTickets { get; set; }
|
||||
public int OpenTickets { get; set; }
|
||||
public int InProgressTickets { get; set; }
|
||||
public int PendingTickets { get; set; }
|
||||
public int ResolvedTickets { get; set; }
|
||||
public int ClosedTickets { get; set; }
|
||||
public int CriticalTickets { get; set; }
|
||||
public int HighTickets { get; set; }
|
||||
public int MediumTickets { get; set; }
|
||||
public int LowTickets { get; set; }
|
||||
public double AverageResponseTimeMinutes { get; set; }
|
||||
public double AverageResolutionTimeMinutes { get; set; }
|
||||
public List<AgentWorkloadDto> AgentWorkloads { get; set; } = new();
|
||||
}
|
||||
|
||||
public class AgentWorkloadDto
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public int ActiveTickets { get; set; }
|
||||
public int ResolvedThisWeek { get; set; }
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace HelpDesk.Models.DTOs;
|
||||
|
||||
public class GroupDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class CreateGroupRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
namespace HelpDesk.Models.DTOs;
|
||||
|
||||
public class ArticleDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public bool IsPublished { get; set; }
|
||||
public Guid CategoryId { get; set; }
|
||||
public string CategoryName { get; set; } = string.Empty;
|
||||
public Guid AuthorId { get; set; }
|
||||
public string AuthorName { get; set; } = string.Empty;
|
||||
public int ViewCount { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class CreateArticleRequest
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public Guid CategoryId { get; set; }
|
||||
public bool IsPublished { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateArticleRequest
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Content { get; set; }
|
||||
public Guid? CategoryId { get; set; }
|
||||
public bool? IsPublished { get; set; }
|
||||
}
|
||||
|
||||
public class CategoryDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public int ArticleCount { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class CreateCategoryRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
namespace HelpDesk.Models.DTOs;
|
||||
|
||||
public class SLADto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public int ResponseTimeHours { get; set; }
|
||||
public int ResolutionTimeHours { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class CreateSLARequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public int ResponseTimeHours { get; set; }
|
||||
public int ResolutionTimeHours { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateSLARequest
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public int? ResponseTimeHours { get; set; }
|
||||
public int? ResolutionTimeHours { get; set; }
|
||||
public bool? IsActive { get; set; }
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
using HelpDesk.Models.Enums;
|
||||
|
||||
namespace HelpDesk.Models.DTOs;
|
||||
|
||||
public class CreateTicketRequest
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public TicketPriority Priority { get; set; } = TicketPriority.Medium;
|
||||
public Guid? GroupId { get; set; }
|
||||
public Guid? SlaPolicyId { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateTicketRequest
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public TicketPriority? Priority { get; set; }
|
||||
}
|
||||
|
||||
public class TicketDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public TicketStatus Status { get; set; }
|
||||
public TicketPriority Priority { get; set; }
|
||||
public string TicketNumber { get; set; } = string.Empty;
|
||||
public Guid RequesterId { get; set; }
|
||||
public string RequesterName { get; set; } = string.Empty;
|
||||
public Guid? AssigneeId { get; set; }
|
||||
public string? AssigneeName { get; set; }
|
||||
public Guid? GroupId { get; set; }
|
||||
public string? GroupName { get; set; }
|
||||
public Guid? SlaPolicyId { get; set; }
|
||||
public string? SlaPolicyName { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public DateTime? ResolvedAt { get; set; }
|
||||
public int CommentCount { get; set; }
|
||||
}
|
||||
|
||||
public class TicketListResponse
|
||||
{
|
||||
public List<TicketDto> Items { get; set; } = new();
|
||||
public int TotalCount { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
|
||||
public class TicketStatsDto
|
||||
{
|
||||
public int TotalTickets { get; set; }
|
||||
public int OpenTickets { get; set; }
|
||||
public int InProgressTickets { get; set; }
|
||||
public int PendingTickets { get; set; }
|
||||
public int ResolvedTickets { get; set; }
|
||||
public int ClosedTickets { get; set; }
|
||||
}
|
||||
|
||||
public class AddCommentRequest
|
||||
{
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public bool IsInternal { get; set; }
|
||||
}
|
||||
|
||||
public class CommentDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid TicketId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public bool IsInternal { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateStatusRequest
|
||||
{
|
||||
public TicketStatus Status { get; set; }
|
||||
}
|
||||
|
||||
public class AssignTicketRequest
|
||||
{
|
||||
public Guid? AssigneeId { get; set; }
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace HelpDesk.Models.Entities;
|
||||
|
||||
[Table("activity_logs")]
|
||||
public class ActivityLog
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string EntityType { get; set; } = string.Empty;
|
||||
|
||||
public int? EntityId { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string Action { get; set; } = string.Empty;
|
||||
|
||||
public string? Details { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public User? User { get; set; }
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace HelpDesk.Models.Entities;
|
||||
|
||||
[Table("groups")]
|
||||
public class Group
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public ICollection<Ticket> Tickets { get; set; } = new List<Ticket>();
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace HelpDesk.Models.Entities;
|
||||
|
||||
[Table("knowledge_base_articles")]
|
||||
public class KnowledgeBaseArticle
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
public Guid CategoryId { get; set; }
|
||||
public Guid AuthorId { get; set; }
|
||||
|
||||
public bool IsPublished { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public KnowledgeBaseCategory Category { get; set; } = null!;
|
||||
public User Author { get; set; } = null!;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace HelpDesk.Models.Entities;
|
||||
|
||||
[Table("knowledge_base_categories")]
|
||||
public class KnowledgeBaseCategory
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public ICollection<KnowledgeBaseArticle> Articles { get; set; } = new List<KnowledgeBaseArticle>();
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace HelpDesk.Models.Entities;
|
||||
|
||||
[Table("sla_policies")]
|
||||
public class SLAPolicy
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
public int ResponseTimeHours { get; set; }
|
||||
public int ResolutionTimeHours { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public ICollection<Ticket> Tickets { get; set; } = new List<Ticket>();
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using HelpDesk.Models.Enums;
|
||||
|
||||
namespace HelpDesk.Models.Entities;
|
||||
|
||||
[Table("tickets")]
|
||||
public class Ticket
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public TicketStatus Status { get; set; } = TicketStatus.Open;
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public TicketPriority Priority { get; set; } = TicketPriority.Medium;
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public string TicketNumber { get; set; } = string.Empty;
|
||||
|
||||
public Guid RequesterId { get; set; }
|
||||
public Guid? AssigneeId { get; set; }
|
||||
public Guid? GroupId { get; set; }
|
||||
public Guid? SlaPolicyId { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? FirstResponseAt { get; set; }
|
||||
public DateTime? ResolvedAt { get; set; }
|
||||
public DateTime? ClosedAt { get; set; }
|
||||
|
||||
// Navigation
|
||||
public User Requester { get; set; } = null!;
|
||||
public User? Assignee { get; set; }
|
||||
public Group? Group { get; set; }
|
||||
public SLAPolicy? SlaPolicy { get; set; }
|
||||
public ICollection<TicketComment> Comments { get; set; } = new List<TicketComment>();
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace HelpDesk.Models.Entities;
|
||||
|
||||
[Table("ticket_comments")]
|
||||
public class TicketComment
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid TicketId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
public bool IsInternal { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public Ticket Ticket { get; set; } = null!;
|
||||
public User User { get; set; } = null!;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using HelpDesk.Models.Enums;
|
||||
|
||||
namespace HelpDesk.Models.Entities;
|
||||
|
||||
[Table("users")]
|
||||
public class User
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(256)]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(500)]
|
||||
public string PasswordHash { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public UserRole Role { get; set; } = UserRole.Customer;
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
[MaxLength(50)]
|
||||
public string? PhoneNumber { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? AvatarUrl { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public ICollection<Ticket> RequestedTickets { get; set; } = new List<Ticket>();
|
||||
public ICollection<Ticket> AssignedTickets { get; set; } = new List<Ticket>();
|
||||
public ICollection<TicketComment> Comments { get; set; } = new List<TicketComment>();
|
||||
public ICollection<KnowledgeBaseArticle> Articles { get; set; } = new List<KnowledgeBaseArticle>();
|
||||
public ICollection<ActivityLog> ActivityLogs { get; set; } = new List<ActivityLog>();
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace HelpDesk.Models.Enums;
|
||||
|
||||
public enum TicketPriority
|
||||
{
|
||||
Low = 0,
|
||||
Medium = 1,
|
||||
High = 2,
|
||||
Critical = 3
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace HelpDesk.Models.Enums;
|
||||
|
||||
public enum TicketStatus
|
||||
{
|
||||
Open = 0,
|
||||
InProgress = 1,
|
||||
Pending = 2,
|
||||
Resolved = 3,
|
||||
Closed = 4
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace HelpDesk.Models.Enums;
|
||||
|
||||
public enum UserRole
|
||||
{
|
||||
Customer = 0,
|
||||
Agent = 1,
|
||||
Admin = 2,
|
||||
SuperAdmin = 3
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,5 +0,0 @@
|
||||
<Solution>
|
||||
<Project Path="HelpDesk.API/HelpDesk.API.csproj" />
|
||||
<Project Path="HelpDesk.Data/HelpDesk.Data.csproj" />
|
||||
<Project Path="HelpDesk.Models/HelpDesk.Models.csproj" />
|
||||
</Solution>
|
||||
Reference in new issue
Block a user