diff --git a/README.md b/README.md index b5bb668..4e0cbb7 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index e0c08cb..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -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"] diff --git a/backend/HelpDesk.API/Controllers/AuthController.cs b/backend/HelpDesk.API/Controllers/AuthController.cs deleted file mode 100644 index 16b5ff5..0000000 --- a/backend/HelpDesk.API/Controllers/AuthController.cs +++ /dev/null @@ -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> 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> 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> 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 - }; -} diff --git a/backend/HelpDesk.API/Controllers/DashboardController.cs b/backend/HelpDesk.API/Controllers/DashboardController.cs deleted file mode 100644 index bd32cae..0000000 --- a/backend/HelpDesk.API/Controllers/DashboardController.cs +++ /dev/null @@ -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> GetStats() - { - var agents = await _userRepository.GetByRoleAsync(UserRole.Agent); - var agentWorkloads = new List(); - - 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 - }); - } -} diff --git a/backend/HelpDesk.API/Controllers/GroupsController.cs b/backend/HelpDesk.API/Controllers/GroupsController.cs deleted file mode 100644 index a7efdee..0000000 --- a/backend/HelpDesk.API/Controllers/GroupsController.cs +++ /dev/null @@ -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>> GetGroups() - { - var groups = await _groupRepository.GetAllAsync(); - return Ok(groups.Select(MapToDto)); - } - - [HttpGet("{id}")] - public async Task> 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> 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> 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 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 - }; -} diff --git a/backend/HelpDesk.API/Controllers/KnowledgeBaseController.cs b/backend/HelpDesk.API/Controllers/KnowledgeBaseController.cs deleted file mode 100644 index 5607fa3..0000000 --- a/backend/HelpDesk.API/Controllers/KnowledgeBaseController.cs +++ /dev/null @@ -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>> GetAllArticles() - { - var articles = await _kbRepository.GetAllArticlesAsync(); - return Ok(articles.Select(MapArticleToDto)); - } - - [HttpGet("articles/published")] - [AllowAnonymous] - public async Task>> GetPublishedArticles() - { - var articles = await _kbRepository.GetPublishedArticlesAsync(); - return Ok(articles.Select(MapArticleToDto)); - } - - [HttpGet("search")] - [AllowAnonymous] - public async Task>> 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> 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> 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> 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 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>> GetCategories() - { - var categories = await _kbRepository.GetAllCategoriesAsync(); - return Ok(categories.Select(MapCategoryToDto)); - } - - [HttpGet("categories/{id}")] - public async Task> 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> 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> 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 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 - }; -} diff --git a/backend/HelpDesk.API/Controllers/SLAController.cs b/backend/HelpDesk.API/Controllers/SLAController.cs deleted file mode 100644 index 490a757..0000000 --- a/backend/HelpDesk.API/Controllers/SLAController.cs +++ /dev/null @@ -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>> GetSLAPolicies() - { - var policies = await _slaRepository.GetAllAsync(); - return Ok(policies.Select(MapToDto)); - } - - [HttpGet("{id}")] - public async Task> 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> 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> 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 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 - }; -} diff --git a/backend/HelpDesk.API/Controllers/TicketsController.cs b/backend/HelpDesk.API/Controllers/TicketsController.cs deleted file mode 100644 index 37f2821..0000000 --- a/backend/HelpDesk.API/Controllers/TicketsController.cs +++ /dev/null @@ -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> 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> 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> 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> 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> 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 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 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 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 - }; -} diff --git a/backend/HelpDesk.API/Controllers/UsersController.cs b/backend/HelpDesk.API/Controllers/UsersController.cs deleted file mode 100644 index 4f7faed..0000000 --- a/backend/HelpDesk.API/Controllers/UsersController.cs +++ /dev/null @@ -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>> GetUsers() - { - var users = await _userRepository.GetAllAsync(); - return Ok(users.Select(MapToDto)); - } - - [HttpGet("agents")] - public async Task>> GetAgents() - { - var agents = await _userRepository.GetByRoleAsync(UserRole.Agent); - return Ok(agents.Select(MapToDto)); - } - - [HttpGet("{id}")] - public async Task> 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> 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 - }; -} diff --git a/backend/HelpDesk.API/HelpDesk.API.csproj b/backend/HelpDesk.API/HelpDesk.API.csproj deleted file mode 100644 index 0f373aa..0000000 --- a/backend/HelpDesk.API/HelpDesk.API.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - net10.0 - enable - enable - true - - - - - - - - - - - - - - - - - diff --git a/backend/HelpDesk.API/HelpDesk.API.http b/backend/HelpDesk.API/HelpDesk.API.http deleted file mode 100644 index d49407f..0000000 --- a/backend/HelpDesk.API/HelpDesk.API.http +++ /dev/null @@ -1,6 +0,0 @@ -@HelpDesk.API_HostAddress = http://localhost:5098 - -GET {{HelpDesk.API_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/backend/HelpDesk.API/Middleware/ExceptionMiddleware.cs b/backend/HelpDesk.API/Middleware/ExceptionMiddleware.cs deleted file mode 100644 index 5322d5c..0000000 --- a/backend/HelpDesk.API/Middleware/ExceptionMiddleware.cs +++ /dev/null @@ -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 _logger; - - public ExceptionMiddleware(RequestDelegate next, ILogger 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 - })); - } -} diff --git a/backend/HelpDesk.API/Program.cs b/backend/HelpDesk.API/Program.cs deleted file mode 100644 index 02faaef..0000000 --- a/backend/HelpDesk.API/Program.cs +++ /dev/null @@ -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(options => - options.UseNpgsql(connectionString)); - -// Repositories -builder.Services.AddRepositories(); - -// Services -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// 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() - } - }); -}); - -var app = builder.Build(); - -// Exception Middleware -app.UseMiddleware(); - -// Swagger -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - -// CORS -app.UseCors("Frontend"); - -// Auth -app.UseAuthentication(); -app.UseAuthorization(); - -// Map Controllers -app.MapControllers(); - -app.Run(); diff --git a/backend/HelpDesk.API/Properties/launchSettings.json b/backend/HelpDesk.API/Properties/launchSettings.json deleted file mode 100644 index b224eb3..0000000 --- a/backend/HelpDesk.API/Properties/launchSettings.json +++ /dev/null @@ -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" - } - } - } -} diff --git a/backend/HelpDesk.API/Services/IJwtService.cs b/backend/HelpDesk.API/Services/IJwtService.cs deleted file mode 100644 index 86f95ab..0000000 --- a/backend/HelpDesk.API/Services/IJwtService.cs +++ /dev/null @@ -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); -} diff --git a/backend/HelpDesk.API/Services/IPasswordService.cs b/backend/HelpDesk.API/Services/IPasswordService.cs deleted file mode 100644 index fba2a8a..0000000 --- a/backend/HelpDesk.API/Services/IPasswordService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace HelpDesk.API.Services; - -public interface IPasswordService -{ - string HashPassword(string password); - bool VerifyPassword(string password, string hashedPassword); -} diff --git a/backend/HelpDesk.API/Services/JwtService.cs b/backend/HelpDesk.API/Services/JwtService.cs deleted file mode 100644 index 3c4828a..0000000 --- a/backend/HelpDesk.API/Services/JwtService.cs +++ /dev/null @@ -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; - } - } -} diff --git a/backend/HelpDesk.API/Services/PasswordService.cs b/backend/HelpDesk.API/Services/PasswordService.cs deleted file mode 100644 index a4129e5..0000000 --- a/backend/HelpDesk.API/Services/PasswordService.cs +++ /dev/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); - } -} diff --git a/backend/HelpDesk.API/appsettings.Development.json b/backend/HelpDesk.API/appsettings.Development.json deleted file mode 100644 index 4383adf..0000000 --- a/backend/HelpDesk.API/appsettings.Development.json +++ /dev/null @@ -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" -} diff --git a/backend/HelpDesk.API/appsettings.json b/backend/HelpDesk.API/appsettings.json deleted file mode 100644 index 986a13c..0000000 --- a/backend/HelpDesk.API/appsettings.json +++ /dev/null @@ -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" -} diff --git a/backend/HelpDesk.Data/DependencyInjection.cs b/backend/HelpDesk.Data/DependencyInjection.cs deleted file mode 100644 index 726986f..0000000 --- a/backend/HelpDesk.Data/DependencyInjection.cs +++ /dev/null @@ -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(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - return services; - } -} diff --git a/backend/HelpDesk.Data/HelpDesk.Data.csproj b/backend/HelpDesk.Data/HelpDesk.Data.csproj deleted file mode 100644 index ee5f0c2..0000000 --- a/backend/HelpDesk.Data/HelpDesk.Data.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net10.0 - enable - enable - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - diff --git a/backend/HelpDesk.Data/HelpDeskDbContext.cs b/backend/HelpDesk.Data/HelpDeskDbContext.cs deleted file mode 100644 index 169be6b..0000000 --- a/backend/HelpDesk.Data/HelpDeskDbContext.cs +++ /dev/null @@ -1,306 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using HelpDesk.Models.Entities; - -namespace HelpDesk.Data; - -public class HelpDeskDbContext : DbContext -{ - public HelpDeskDbContext(DbContextOptions options) : base(options) - { - } - - public DbSet Users => Set(); - public DbSet Tickets => Set(); - public DbSet TicketComments => Set(); - public DbSet Groups => Set(); - public DbSet SLAPolicies => Set(); - public DbSet KnowledgeBaseArticles => Set(); - public DbSet KnowledgeBaseCategories => Set(); - public DbSet ActivityLogs => Set(); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - // ── User ────────────────────────────────────────────── - modelBuilder.Entity(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(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(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(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(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(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(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(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().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().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().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().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) - } - ); - } -} diff --git a/backend/HelpDesk.Data/Repositories/IKnowledgeBaseRepository.cs b/backend/HelpDesk.Data/Repositories/IKnowledgeBaseRepository.cs deleted file mode 100644 index fbadeba..0000000 --- a/backend/HelpDesk.Data/Repositories/IKnowledgeBaseRepository.cs +++ /dev/null @@ -1,22 +0,0 @@ -using HelpDesk.Models.Entities; - -namespace HelpDesk.Data.Repositories; - -public interface IKnowledgeBaseRepository -{ - // Articles - Task GetArticleByIdAsync(Guid id); - Task> GetAllArticlesAsync(); - Task> GetPublishedArticlesAsync(); - Task> SearchArticlesAsync(string query); - Task CreateArticleAsync(KnowledgeBaseArticle article); - Task UpdateArticleAsync(KnowledgeBaseArticle article); - Task DeleteArticleAsync(Guid id); - - // Categories - Task GetCategoryByIdAsync(Guid id); - Task> GetAllCategoriesAsync(); - Task CreateCategoryAsync(KnowledgeBaseCategory category); - Task UpdateCategoryAsync(KnowledgeBaseCategory category); - Task DeleteCategoryAsync(Guid id); -} diff --git a/backend/HelpDesk.Data/Repositories/IRepository.cs b/backend/HelpDesk.Data/Repositories/IRepository.cs deleted file mode 100644 index da7e117..0000000 --- a/backend/HelpDesk.Data/Repositories/IRepository.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace HelpDesk.Data.Repositories; - -public interface IRepository where T : class -{ - Task GetByIdAsync(Guid id); - Task> GetAllAsync(); - Task AddAsync(T entity); - Task UpdateAsync(T entity); - Task DeleteAsync(T entity); - Task SaveChangesAsync(); -} diff --git a/backend/HelpDesk.Data/Repositories/ITicketRepository.cs b/backend/HelpDesk.Data/Repositories/ITicketRepository.cs deleted file mode 100644 index 39f0ade..0000000 --- a/backend/HelpDesk.Data/Repositories/ITicketRepository.cs +++ /dev/null @@ -1,19 +0,0 @@ -using HelpDesk.Models.Entities; -using HelpDesk.Models.Enums; - -namespace HelpDesk.Data.Repositories; - -public interface ITicketRepository -{ - Task GetByIdAsync(Guid id); - Task> GetAllAsync(TicketStatus? status = null, TicketPriority? priority = null, Guid? assigneeId = null, Guid? requesterId = null, int page = 1, int pageSize = 20); - Task GetCountAsync(TicketStatus? status = null, TicketPriority? priority = null, Guid? assigneeId = null, Guid? requesterId = null); - Task CreateAsync(Ticket ticket); - Task UpdateAsync(Ticket ticket); - Task DeleteAsync(Guid id); - Task GetTotalCountAsync(); - Task GetCountByStatusAsync(TicketStatus status); - Task GetCountByPriorityAsync(TicketPriority priority); - Task GetAverageResponseTimeAsync(); - Task GetAverageResolutionTimeAsync(); -} diff --git a/backend/HelpDesk.Data/Repositories/IUserRepository.cs b/backend/HelpDesk.Data/Repositories/IUserRepository.cs deleted file mode 100644 index 68fecfe..0000000 --- a/backend/HelpDesk.Data/Repositories/IUserRepository.cs +++ /dev/null @@ -1,15 +0,0 @@ -using HelpDesk.Models.Entities; -using HelpDesk.Models.Enums; - -namespace HelpDesk.Data.Repositories; - -public interface IUserRepository -{ - Task GetByIdAsync(Guid id); - Task GetByEmailAsync(string email); - Task> GetAllAsync(); - Task> GetByRoleAsync(UserRole role); - Task CreateAsync(User user); - Task UpdateAsync(User user); - Task ExistsAsync(string email); -} diff --git a/backend/HelpDesk.Data/Repositories/KnowledgeBaseRepository.cs b/backend/HelpDesk.Data/Repositories/KnowledgeBaseRepository.cs deleted file mode 100644 index 8ff2ba2..0000000 --- a/backend/HelpDesk.Data/Repositories/KnowledgeBaseRepository.cs +++ /dev/null @@ -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 GetArticleByIdAsync(Guid id) - => await _context.KnowledgeBaseArticles - .Include(a => a.Category) - .Include(a => a.Author) - .FirstOrDefaultAsync(a => a.Id == id); - - public async Task> GetAllArticlesAsync() - => await _context.KnowledgeBaseArticles - .Include(a => a.Category) - .Include(a => a.Author) - .OrderByDescending(a => a.CreatedAt) - .ToListAsync(); - - public async Task> GetPublishedArticlesAsync() - => await _context.KnowledgeBaseArticles - .Include(a => a.Category) - .Include(a => a.Author) - .Where(a => a.IsPublished) - .OrderByDescending(a => a.CreatedAt) - .ToListAsync(); - - public async Task> 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 CreateArticleAsync(KnowledgeBaseArticle article) - { - _context.KnowledgeBaseArticles.Add(article); - await _context.SaveChangesAsync(); - return article; - } - - public async Task 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 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 GetCategoryByIdAsync(Guid id) - => await _context.KnowledgeBaseCategories - .Include(c => c.Articles) - .FirstOrDefaultAsync(c => c.Id == id); - - public async Task> GetAllCategoriesAsync() - => await _context.KnowledgeBaseCategories - .Include(c => c.Articles) - .OrderBy(c => c.SortOrder) - .ThenBy(c => c.Name) - .ToListAsync(); - - public async Task CreateCategoryAsync(KnowledgeBaseCategory category) - { - _context.KnowledgeBaseCategories.Add(category); - await _context.SaveChangesAsync(); - return category; - } - - public async Task 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 DeleteCategoryAsync(Guid id) - { - var category = await _context.KnowledgeBaseCategories.FindAsync(id); - if (category == null) return false; - _context.KnowledgeBaseCategories.Remove(category); - await _context.SaveChangesAsync(); - return true; - } -} diff --git a/backend/HelpDesk.Data/Repositories/OtherRepositories.cs b/backend/HelpDesk.Data/Repositories/OtherRepositories.cs deleted file mode 100644 index ac80d76..0000000 --- a/backend/HelpDesk.Data/Repositories/OtherRepositories.cs +++ /dev/null @@ -1,29 +0,0 @@ -using HelpDesk.Models.Entities; - -namespace HelpDesk.Data.Repositories; - -public interface ITicketCommentRepository -{ - Task GetByIdAsync(Guid id); - Task> GetByTicketIdAsync(Guid ticketId); - Task CreateAsync(TicketComment comment); - Task DeleteAsync(Guid id); -} - -public interface IGroupRepository -{ - Task GetByIdAsync(Guid id); - Task> GetAllAsync(); - Task CreateAsync(Group group); - Task UpdateAsync(Group group); - Task DeleteAsync(Guid id); -} - -public interface ISLARepository -{ - Task GetByIdAsync(Guid id); - Task> GetAllAsync(); - Task CreateAsync(SLAPolicy policy); - Task UpdateAsync(SLAPolicy policy); - Task DeleteAsync(Guid id); -} diff --git a/backend/HelpDesk.Data/Repositories/Repository.cs b/backend/HelpDesk.Data/Repositories/Repository.cs deleted file mode 100644 index 31a71f8..0000000 --- a/backend/HelpDesk.Data/Repositories/Repository.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace HelpDesk.Data.Repositories; - -public class Repository : IRepository where T : class -{ - protected readonly HelpDeskDbContext _context; - protected readonly DbSet _dbSet; - - public Repository(HelpDeskDbContext context) - { - _context = context; - _dbSet = context.Set(); - } - - public virtual async Task GetByIdAsync(Guid id) - { - return await _dbSet.FindAsync(id); - } - - public virtual async Task> GetAllAsync() - { - return await _dbSet.ToListAsync(); - } - - public virtual async Task 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 SaveChangesAsync() - { - return await _context.SaveChangesAsync(); - } -} diff --git a/backend/HelpDesk.Data/Repositories/TicketCommentGroupSlaRepositories.cs b/backend/HelpDesk.Data/Repositories/TicketCommentGroupSlaRepositories.cs deleted file mode 100644 index 9d5cd23..0000000 --- a/backend/HelpDesk.Data/Repositories/TicketCommentGroupSlaRepositories.cs +++ /dev/null @@ -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 GetByIdAsync(Guid id) - => await _context.TicketComments.FindAsync(id); - - public async Task> GetByTicketIdAsync(Guid ticketId) - => await _context.TicketComments - .Include(c => c.User) - .Where(c => c.TicketId == ticketId) - .OrderBy(c => c.CreatedAt) - .ToListAsync(); - - public async Task CreateAsync(TicketComment comment) - { - _context.TicketComments.Add(comment); - await _context.SaveChangesAsync(); - return comment; - } - - public async Task 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 GetByIdAsync(Guid id) - => await _context.Groups.FindAsync(id); - - public async Task> GetAllAsync() - => await _context.Groups.OrderBy(g => g.Name).ToListAsync(); - - public async Task CreateAsync(Group group) - { - _context.Groups.Add(group); - await _context.SaveChangesAsync(); - return group; - } - - public async Task 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 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 GetByIdAsync(Guid id) - => await _context.SLAPolicies.FindAsync(id); - - public async Task> GetAllAsync() - => await _context.SLAPolicies.OrderBy(s => s.Name).ToListAsync(); - - public async Task CreateAsync(SLAPolicy policy) - { - _context.SLAPolicies.Add(policy); - await _context.SaveChangesAsync(); - return policy; - } - - public async Task 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 DeleteAsync(Guid id) - { - var policy = await _context.SLAPolicies.FindAsync(id); - if (policy == null) return false; - _context.SLAPolicies.Remove(policy); - await _context.SaveChangesAsync(); - return true; - } -} diff --git a/backend/HelpDesk.Data/Repositories/TicketRepository.cs b/backend/HelpDesk.Data/Repositories/TicketRepository.cs deleted file mode 100644 index ca50aa1..0000000 --- a/backend/HelpDesk.Data/Repositories/TicketRepository.cs +++ /dev/null @@ -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 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 GetByIdAsync(Guid id) - => await GetQueryable().FirstOrDefaultAsync(t => t.Id == id); - - public async Task> 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 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 CreateAsync(Ticket ticket) - { - _context.Tickets.Add(ticket); - await _context.SaveChangesAsync(); - return ticket; - } - - public async Task 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 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 GetTotalCountAsync() - => await _context.Tickets.CountAsync(); - - public async Task GetCountByStatusAsync(TicketStatus status) - => await _context.Tickets.CountAsync(t => t.Status == status); - - public async Task GetCountByPriorityAsync(TicketPriority priority) - => await _context.Tickets.CountAsync(t => t.Priority == priority); - - public async Task 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 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); - } -} diff --git a/backend/HelpDesk.Data/Repositories/UserRepository.cs b/backend/HelpDesk.Data/Repositories/UserRepository.cs deleted file mode 100644 index 33127f6..0000000 --- a/backend/HelpDesk.Data/Repositories/UserRepository.cs +++ /dev/null @@ -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 GetByIdAsync(Guid id) - => await _context.Users.FirstOrDefaultAsync(u => u.Id == id); - - public async Task GetByEmailAsync(string email) - => await _context.Users.FirstOrDefaultAsync(u => u.Email == email); - - public async Task> GetAllAsync() - => await _context.Users.OrderBy(u => u.LastName).ToListAsync(); - - public async Task> GetByRoleAsync(UserRole role) - => await _context.Users.Where(u => u.Role == role && u.IsActive).OrderBy(u => u.LastName).ToListAsync(); - - public async Task CreateAsync(User user) - { - _context.Users.Add(user); - await _context.SaveChangesAsync(); - return user; - } - - public async Task 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 ExistsAsync(string email) - => await _context.Users.AnyAsync(u => u.Email == email); -} diff --git a/backend/HelpDesk.Models/DTOs/AuthDtos.cs b/backend/HelpDesk.Models/DTOs/AuthDtos.cs deleted file mode 100644 index 979d8a0..0000000 --- a/backend/HelpDesk.Models/DTOs/AuthDtos.cs +++ /dev/null @@ -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; } -} diff --git a/backend/HelpDesk.Models/DTOs/DashboardDtos.cs b/backend/HelpDesk.Models/DTOs/DashboardDtos.cs deleted file mode 100644 index 01f916f..0000000 --- a/backend/HelpDesk.Models/DTOs/DashboardDtos.cs +++ /dev/null @@ -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 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; } -} diff --git a/backend/HelpDesk.Models/DTOs/GroupDtos.cs b/backend/HelpDesk.Models/DTOs/GroupDtos.cs deleted file mode 100644 index 7328701..0000000 --- a/backend/HelpDesk.Models/DTOs/GroupDtos.cs +++ /dev/null @@ -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; } -} diff --git a/backend/HelpDesk.Models/DTOs/KnowledgeBaseDtos.cs b/backend/HelpDesk.Models/DTOs/KnowledgeBaseDtos.cs deleted file mode 100644 index da5c546..0000000 --- a/backend/HelpDesk.Models/DTOs/KnowledgeBaseDtos.cs +++ /dev/null @@ -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; } -} diff --git a/backend/HelpDesk.Models/DTOs/SLADtos.cs b/backend/HelpDesk.Models/DTOs/SLADtos.cs deleted file mode 100644 index a12f3f2..0000000 --- a/backend/HelpDesk.Models/DTOs/SLADtos.cs +++ /dev/null @@ -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; } -} diff --git a/backend/HelpDesk.Models/DTOs/TicketDtos.cs b/backend/HelpDesk.Models/DTOs/TicketDtos.cs deleted file mode 100644 index dde2ab5..0000000 --- a/backend/HelpDesk.Models/DTOs/TicketDtos.cs +++ /dev/null @@ -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 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; } -} diff --git a/backend/HelpDesk.Models/Entities/ActivityLog.cs b/backend/HelpDesk.Models/Entities/ActivityLog.cs deleted file mode 100644 index 1aab992..0000000 --- a/backend/HelpDesk.Models/Entities/ActivityLog.cs +++ /dev/null @@ -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; } -} diff --git a/backend/HelpDesk.Models/Entities/Group.cs b/backend/HelpDesk.Models/Entities/Group.cs deleted file mode 100644 index a152191..0000000 --- a/backend/HelpDesk.Models/Entities/Group.cs +++ /dev/null @@ -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 Tickets { get; set; } = new List(); -} diff --git a/backend/HelpDesk.Models/Entities/KnowledgeBaseArticle.cs b/backend/HelpDesk.Models/Entities/KnowledgeBaseArticle.cs deleted file mode 100644 index 0e835b5..0000000 --- a/backend/HelpDesk.Models/Entities/KnowledgeBaseArticle.cs +++ /dev/null @@ -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!; -} diff --git a/backend/HelpDesk.Models/Entities/KnowledgeBaseCategory.cs b/backend/HelpDesk.Models/Entities/KnowledgeBaseCategory.cs deleted file mode 100644 index 6ce9ac3..0000000 --- a/backend/HelpDesk.Models/Entities/KnowledgeBaseCategory.cs +++ /dev/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 Articles { get; set; } = new List(); -} diff --git a/backend/HelpDesk.Models/Entities/SLAPolicy.cs b/backend/HelpDesk.Models/Entities/SLAPolicy.cs deleted file mode 100644 index bcf83de..0000000 --- a/backend/HelpDesk.Models/Entities/SLAPolicy.cs +++ /dev/null @@ -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 Tickets { get; set; } = new List(); -} diff --git a/backend/HelpDesk.Models/Entities/Ticket.cs b/backend/HelpDesk.Models/Entities/Ticket.cs deleted file mode 100644 index 5ae8d02..0000000 --- a/backend/HelpDesk.Models/Entities/Ticket.cs +++ /dev/null @@ -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 Comments { get; set; } = new List(); -} diff --git a/backend/HelpDesk.Models/Entities/TicketComment.cs b/backend/HelpDesk.Models/Entities/TicketComment.cs deleted file mode 100644 index 8aa03c5..0000000 --- a/backend/HelpDesk.Models/Entities/TicketComment.cs +++ /dev/null @@ -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!; -} diff --git a/backend/HelpDesk.Models/Entities/User.cs b/backend/HelpDesk.Models/Entities/User.cs deleted file mode 100644 index 4db9e32..0000000 --- a/backend/HelpDesk.Models/Entities/User.cs +++ /dev/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 RequestedTickets { get; set; } = new List(); - public ICollection AssignedTickets { get; set; } = new List(); - public ICollection Comments { get; set; } = new List(); - public ICollection Articles { get; set; } = new List(); - public ICollection ActivityLogs { get; set; } = new List(); -} diff --git a/backend/HelpDesk.Models/Enums/TicketPriority.cs b/backend/HelpDesk.Models/Enums/TicketPriority.cs deleted file mode 100644 index 15cc65b..0000000 --- a/backend/HelpDesk.Models/Enums/TicketPriority.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace HelpDesk.Models.Enums; - -public enum TicketPriority -{ - Low = 0, - Medium = 1, - High = 2, - Critical = 3 -} diff --git a/backend/HelpDesk.Models/Enums/TicketStatus.cs b/backend/HelpDesk.Models/Enums/TicketStatus.cs deleted file mode 100644 index 24f5934..0000000 --- a/backend/HelpDesk.Models/Enums/TicketStatus.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace HelpDesk.Models.Enums; - -public enum TicketStatus -{ - Open = 0, - InProgress = 1, - Pending = 2, - Resolved = 3, - Closed = 4 -} diff --git a/backend/HelpDesk.Models/Enums/UserRole.cs b/backend/HelpDesk.Models/Enums/UserRole.cs deleted file mode 100644 index 9e72055..0000000 --- a/backend/HelpDesk.Models/Enums/UserRole.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace HelpDesk.Models.Enums; - -public enum UserRole -{ - Customer = 0, - Agent = 1, - Admin = 2, - SuperAdmin = 3 -} diff --git a/backend/HelpDesk.Models/HelpDesk.Models.csproj b/backend/HelpDesk.Models/HelpDesk.Models.csproj deleted file mode 100644 index b760144..0000000 --- a/backend/HelpDesk.Models/HelpDesk.Models.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net10.0 - enable - enable - - - diff --git a/backend/HelpDesk.slnx b/backend/HelpDesk.slnx deleted file mode 100644 index e43c2e3..0000000 --- a/backend/HelpDesk.slnx +++ /dev/null @@ -1,5 +0,0 @@ - - - - -