Initial commit: HelpDesk ICHN - .NET 10 backend + Next.js 16 frontend
This commit is contained in:
commit
359f0caf85
90 files changed
+13395
No files matched your search
@@ -0,0 +1,18 @@
|
||||
using HelpDesk.Data.Repositories;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HelpDesk.Data;
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddRepositories(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
services.AddScoped<ITicketRepository, TicketRepository>();
|
||||
services.AddScoped<ITicketCommentRepository, TicketCommentRepository>();
|
||||
services.AddScoped<IGroupRepository, GroupRepository>();
|
||||
services.AddScoped<ISLARepository, SLARepository>();
|
||||
services.AddScoped<IKnowledgeBaseRepository, KnowledgeBaseRepository>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HelpDesk.Models\HelpDesk.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,306 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using HelpDesk.Models.Entities;
|
||||
|
||||
namespace HelpDesk.Data;
|
||||
|
||||
public class HelpDeskDbContext : DbContext
|
||||
{
|
||||
public HelpDeskDbContext(DbContextOptions<HelpDeskDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<Ticket> Tickets => Set<Ticket>();
|
||||
public DbSet<TicketComment> TicketComments => Set<TicketComment>();
|
||||
public DbSet<Group> Groups => Set<Group>();
|
||||
public DbSet<SLAPolicy> SLAPolicies => Set<SLAPolicy>();
|
||||
public DbSet<KnowledgeBaseArticle> KnowledgeBaseArticles => Set<KnowledgeBaseArticle>();
|
||||
public DbSet<KnowledgeBaseCategory> KnowledgeBaseCategories => Set<KnowledgeBaseCategory>();
|
||||
public DbSet<ActivityLog> ActivityLogs => Set<ActivityLog>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// ── User ──────────────────────────────────────────────
|
||||
modelBuilder.Entity<User>(entity =>
|
||||
{
|
||||
entity.ToTable("users");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.FirstName).HasColumnName("first_name").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.LastName).HasColumnName("last_name").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.Email).HasColumnName("email").HasMaxLength(256).IsRequired();
|
||||
entity.Property(e => e.PasswordHash).HasColumnName("password_hash").HasMaxLength(500).IsRequired();
|
||||
entity.Property(e => e.Role).HasColumnName("role").HasMaxLength(50).IsRequired();
|
||||
entity.Property(e => e.IsActive).HasColumnName("is_active");
|
||||
entity.Property(e => e.PhoneNumber).HasColumnName("phone_number").HasMaxLength(50);
|
||||
entity.Property(e => e.AvatarUrl).HasColumnName("avatar_url").HasMaxLength(500);
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
|
||||
entity.HasIndex(e => e.Email).IsUnique().HasDatabaseName("ix_users_email");
|
||||
});
|
||||
|
||||
// ── Ticket ────────────────────────────────────────────
|
||||
modelBuilder.Entity<Ticket>(entity =>
|
||||
{
|
||||
entity.ToTable("tickets");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.Title).HasColumnName("title").HasMaxLength(200).IsRequired();
|
||||
entity.Property(e => e.Description).HasColumnName("description").IsRequired();
|
||||
entity.Property(e => e.Status).HasColumnName("status").HasMaxLength(50).IsRequired();
|
||||
entity.Property(e => e.Priority).HasColumnName("priority").HasMaxLength(50).IsRequired();
|
||||
entity.Property(e => e.TicketNumber).HasColumnName("ticket_number").HasMaxLength(50).IsRequired();
|
||||
entity.Property(e => e.RequesterId).HasColumnName("requester_id");
|
||||
entity.Property(e => e.AssigneeId).HasColumnName("assignee_id");
|
||||
entity.Property(e => e.GroupId).HasColumnName("group_id");
|
||||
entity.Property(e => e.SlaPolicyId).HasColumnName("sla_policy_id");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
entity.Property(e => e.ResolvedAt).HasColumnName("resolved_at");
|
||||
entity.Property(e => e.ClosedAt).HasColumnName("closed_at");
|
||||
|
||||
entity.HasIndex(e => e.TicketNumber).IsUnique().HasDatabaseName("ix_tickets_ticket_number");
|
||||
|
||||
// Cascade delete: Ticket -> TicketComment
|
||||
entity.HasMany(e => e.Comments)
|
||||
.WithOne(c => c.Ticket)
|
||||
.HasForeignKey(c => c.TicketId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Restrict: User FK references (Requester)
|
||||
entity.HasOne(e => e.Requester)
|
||||
.WithMany(u => u.RequestedTickets)
|
||||
.HasForeignKey(e => e.RequesterId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// Restrict: User FK references (Assignee)
|
||||
entity.HasOne(e => e.Assignee)
|
||||
.WithMany(u => u.AssignedTickets)
|
||||
.HasForeignKey(e => e.AssigneeId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
entity.HasOne(e => e.Group)
|
||||
.WithMany(g => g.Tickets)
|
||||
.HasForeignKey(e => e.GroupId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
entity.HasOne(e => e.SlaPolicy)
|
||||
.WithMany(s => s.Tickets)
|
||||
.HasForeignKey(e => e.SlaPolicyId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
// ── TicketComment ─────────────────────────────────────
|
||||
modelBuilder.Entity<TicketComment>(entity =>
|
||||
{
|
||||
entity.ToTable("ticket_comments");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.TicketId).HasColumnName("ticket_id");
|
||||
entity.Property(e => e.UserId).HasColumnName("user_id");
|
||||
entity.Property(e => e.Content).HasColumnName("content").IsRequired();
|
||||
entity.Property(e => e.IsInternal).HasColumnName("is_internal");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
|
||||
// Restrict: User FK reference
|
||||
entity.HasOne(e => e.User)
|
||||
.WithMany(u => u.Comments)
|
||||
.HasForeignKey(e => e.UserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
// ── Group ─────────────────────────────────────────────
|
||||
modelBuilder.Entity<Group>(entity =>
|
||||
{
|
||||
entity.ToTable("groups");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.Name).HasColumnName("name").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.Description).HasColumnName("description").HasMaxLength(500);
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
});
|
||||
|
||||
// ── SLAPolicy ─────────────────────────────────────────
|
||||
modelBuilder.Entity<SLAPolicy>(entity =>
|
||||
{
|
||||
entity.ToTable("sla_policies");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.Name).HasColumnName("name").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.Description).HasColumnName("description").HasMaxLength(500);
|
||||
entity.Property(e => e.ResponseTimeHours).HasColumnName("response_time_hours");
|
||||
entity.Property(e => e.ResolutionTimeHours).HasColumnName("resolution_time_hours");
|
||||
entity.Property(e => e.IsActive).HasColumnName("is_active");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
});
|
||||
|
||||
// ── KnowledgeBaseArticle ──────────────────────────────
|
||||
modelBuilder.Entity<KnowledgeBaseArticle>(entity =>
|
||||
{
|
||||
entity.ToTable("knowledge_base_articles");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.Title).HasColumnName("title").HasMaxLength(200).IsRequired();
|
||||
entity.Property(e => e.Content).HasColumnName("content").IsRequired();
|
||||
entity.Property(e => e.CategoryId).HasColumnName("category_id");
|
||||
entity.Property(e => e.AuthorId).HasColumnName("author_id");
|
||||
entity.Property(e => e.IsPublished).HasColumnName("is_published");
|
||||
entity.Property(e => e.ViewCount).HasColumnName("view_count");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
|
||||
entity.HasOne(e => e.Category)
|
||||
.WithMany(c => c.Articles)
|
||||
.HasForeignKey(e => e.CategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
entity.HasOne(e => e.Author)
|
||||
.WithMany(u => u.Articles)
|
||||
.HasForeignKey(e => e.AuthorId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
// ── KnowledgeBaseCategory ─────────────────────────────
|
||||
modelBuilder.Entity<KnowledgeBaseCategory>(entity =>
|
||||
{
|
||||
entity.ToTable("knowledge_base_categories");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.Name).HasColumnName("name").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.Description).HasColumnName("description").HasMaxLength(500);
|
||||
entity.Property(e => e.SortOrder).HasColumnName("sort_order");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
});
|
||||
|
||||
// ── ActivityLog ───────────────────────────────────────
|
||||
modelBuilder.Entity<ActivityLog>(entity =>
|
||||
{
|
||||
entity.ToTable("activity_logs");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.UserId).HasColumnName("user_id");
|
||||
entity.Property(e => e.EntityType).HasColumnName("entity_type").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.EntityId).HasColumnName("entity_id");
|
||||
entity.Property(e => e.Action).HasColumnName("action").HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.Details).HasColumnName("details");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
|
||||
entity.HasOne(e => e.User)
|
||||
.WithMany(u => u.ActivityLogs)
|
||||
.HasForeignKey(e => e.UserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
// ── Seed Data ─────────────────────────────────────────
|
||||
SeedData(modelBuilder);
|
||||
}
|
||||
|
||||
private static void SeedData(ModelBuilder modelBuilder)
|
||||
{
|
||||
var adminId = Guid.Parse("a0000000-0000-0000-0000-000000000001");
|
||||
var generalGroupId = Guid.Parse("b0000000-0000-0000-0000-000000000001");
|
||||
var ventasGroupId = Guid.Parse("b0000000-0000-0000-0000-000000000002");
|
||||
var desarrolloGroupId = Guid.Parse("b0000000-0000-0000-0000-000000000003");
|
||||
var standardSlaId = Guid.Parse("c0000000-0000-0000-0000-000000000001");
|
||||
var premiumSlaId = Guid.Parse("c0000000-0000-0000-0000-000000000002");
|
||||
var generalCategoryId = Guid.Parse("d0000000-0000-0000-0000-000000000001");
|
||||
var faqCategoryId = Guid.Parse("d0000000-0000-0000-0000-000000000002");
|
||||
var tutorialsCategoryId = Guid.Parse("d0000000-0000-0000-0000-000000000003");
|
||||
|
||||
// SuperAdmin user
|
||||
modelBuilder.Entity<User>().HasData(new User
|
||||
{
|
||||
Id = adminId,
|
||||
FirstName = "Super",
|
||||
LastName = "Admin",
|
||||
Email = "admin@helpdesk.local",
|
||||
// BCrypt hash for "Admin123!"
|
||||
PasswordHash = "$2a$11$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy",
|
||||
Role = HelpDesk.Models.Enums.UserRole.SuperAdmin,
|
||||
IsActive = true,
|
||||
PhoneNumber = "+1-555-0100",
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
UpdatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
});
|
||||
|
||||
// Default groups
|
||||
modelBuilder.Entity<Group>().HasData(
|
||||
new Group
|
||||
{
|
||||
Id = generalGroupId,
|
||||
Name = "Soporte General",
|
||||
Description = "Grupo de soporte técnico general",
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
},
|
||||
new Group
|
||||
{
|
||||
Id = ventasGroupId,
|
||||
Name = "Ventas",
|
||||
Description = "Grupo de soporte de ventas",
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
},
|
||||
new Group
|
||||
{
|
||||
Id = desarrolloGroupId,
|
||||
Name = "Desarrollo",
|
||||
Description = "Grupo de desarrollo de software",
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
}
|
||||
);
|
||||
|
||||
// SLA policies
|
||||
modelBuilder.Entity<SLAPolicy>().HasData(
|
||||
new SLAPolicy
|
||||
{
|
||||
Id = standardSlaId,
|
||||
Name = "Estándar",
|
||||
Description = "Política estándar: respuesta en 4 horas, resolución en 24 horas",
|
||||
ResponseTimeHours = 4,
|
||||
ResolutionTimeHours = 24,
|
||||
IsActive = true,
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
},
|
||||
new SLAPolicy
|
||||
{
|
||||
Id = premiumSlaId,
|
||||
Name = "Premium",
|
||||
Description = "Política premium: respuesta en 1 hora, resolución en 8 horas",
|
||||
ResponseTimeHours = 1,
|
||||
ResolutionTimeHours = 8,
|
||||
IsActive = true,
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
}
|
||||
);
|
||||
|
||||
// Knowledge base categories
|
||||
modelBuilder.Entity<KnowledgeBaseCategory>().HasData(
|
||||
new KnowledgeBaseCategory
|
||||
{
|
||||
Id = generalCategoryId,
|
||||
Name = "General",
|
||||
Description = "Artículos generales de ayuda",
|
||||
SortOrder = 1,
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
},
|
||||
new KnowledgeBaseCategory
|
||||
{
|
||||
Id = faqCategoryId,
|
||||
Name = "Preguntas Frecuentes",
|
||||
Description = "Preguntas frecuentes y respuestas",
|
||||
SortOrder = 2,
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
},
|
||||
new KnowledgeBaseCategory
|
||||
{
|
||||
Id = tutorialsCategoryId,
|
||||
Name = "Tutoriales",
|
||||
Description = "Guías y tutoriales paso a paso",
|
||||
SortOrder = 3,
|
||||
CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public interface IKnowledgeBaseRepository
|
||||
{
|
||||
// Articles
|
||||
Task<KnowledgeBaseArticle?> GetArticleByIdAsync(Guid id);
|
||||
Task<IEnumerable<KnowledgeBaseArticle>> GetAllArticlesAsync();
|
||||
Task<IEnumerable<KnowledgeBaseArticle>> GetPublishedArticlesAsync();
|
||||
Task<IEnumerable<KnowledgeBaseArticle>> SearchArticlesAsync(string query);
|
||||
Task<KnowledgeBaseArticle> CreateArticleAsync(KnowledgeBaseArticle article);
|
||||
Task<KnowledgeBaseArticle?> UpdateArticleAsync(KnowledgeBaseArticle article);
|
||||
Task<bool> DeleteArticleAsync(Guid id);
|
||||
|
||||
// Categories
|
||||
Task<KnowledgeBaseCategory?> GetCategoryByIdAsync(Guid id);
|
||||
Task<IEnumerable<KnowledgeBaseCategory>> GetAllCategoriesAsync();
|
||||
Task<KnowledgeBaseCategory> CreateCategoryAsync(KnowledgeBaseCategory category);
|
||||
Task<KnowledgeBaseCategory?> UpdateCategoryAsync(KnowledgeBaseCategory category);
|
||||
Task<bool> DeleteCategoryAsync(Guid id);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public interface IRepository<T> where T : class
|
||||
{
|
||||
Task<T?> GetByIdAsync(Guid id);
|
||||
Task<IEnumerable<T>> GetAllAsync();
|
||||
Task<T> AddAsync(T entity);
|
||||
Task UpdateAsync(T entity);
|
||||
Task DeleteAsync(T entity);
|
||||
Task<int> SaveChangesAsync();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
using HelpDesk.Models.Enums;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public interface ITicketRepository
|
||||
{
|
||||
Task<Ticket?> GetByIdAsync(Guid id);
|
||||
Task<IEnumerable<Ticket>> GetAllAsync(TicketStatus? status = null, TicketPriority? priority = null, Guid? assigneeId = null, Guid? requesterId = null, int page = 1, int pageSize = 20);
|
||||
Task<int> GetCountAsync(TicketStatus? status = null, TicketPriority? priority = null, Guid? assigneeId = null, Guid? requesterId = null);
|
||||
Task<Ticket> CreateAsync(Ticket ticket);
|
||||
Task<Ticket?> UpdateAsync(Ticket ticket);
|
||||
Task<bool> DeleteAsync(Guid id);
|
||||
Task<int> GetTotalCountAsync();
|
||||
Task<int> GetCountByStatusAsync(TicketStatus status);
|
||||
Task<int> GetCountByPriorityAsync(TicketPriority priority);
|
||||
Task<double> GetAverageResponseTimeAsync();
|
||||
Task<double> GetAverageResolutionTimeAsync();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
using HelpDesk.Models.Enums;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public interface IUserRepository
|
||||
{
|
||||
Task<User?> GetByIdAsync(Guid id);
|
||||
Task<User?> GetByEmailAsync(string email);
|
||||
Task<IEnumerable<User>> GetAllAsync();
|
||||
Task<IEnumerable<User>> GetByRoleAsync(UserRole role);
|
||||
Task<User> CreateAsync(User user);
|
||||
Task<User?> UpdateAsync(User user);
|
||||
Task<bool> ExistsAsync(string email);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public class KnowledgeBaseRepository : IKnowledgeBaseRepository
|
||||
{
|
||||
private readonly HelpDeskDbContext _context;
|
||||
|
||||
public KnowledgeBaseRepository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// Articles
|
||||
public async Task<KnowledgeBaseArticle?> GetArticleByIdAsync(Guid id)
|
||||
=> await _context.KnowledgeBaseArticles
|
||||
.Include(a => a.Category)
|
||||
.Include(a => a.Author)
|
||||
.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
||||
public async Task<IEnumerable<KnowledgeBaseArticle>> GetAllArticlesAsync()
|
||||
=> await _context.KnowledgeBaseArticles
|
||||
.Include(a => a.Category)
|
||||
.Include(a => a.Author)
|
||||
.OrderByDescending(a => a.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<IEnumerable<KnowledgeBaseArticle>> GetPublishedArticlesAsync()
|
||||
=> await _context.KnowledgeBaseArticles
|
||||
.Include(a => a.Category)
|
||||
.Include(a => a.Author)
|
||||
.Where(a => a.IsPublished)
|
||||
.OrderByDescending(a => a.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<IEnumerable<KnowledgeBaseArticle>> SearchArticlesAsync(string query)
|
||||
{
|
||||
var lowerQuery = query.ToLower();
|
||||
return await _context.KnowledgeBaseArticles
|
||||
.Include(a => a.Category)
|
||||
.Include(a => a.Author)
|
||||
.Where(a => a.IsPublished && (a.Title.ToLower().Contains(lowerQuery) || a.Content.ToLower().Contains(lowerQuery)))
|
||||
.OrderByDescending(a => a.ViewCount)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<KnowledgeBaseArticle> CreateArticleAsync(KnowledgeBaseArticle article)
|
||||
{
|
||||
_context.KnowledgeBaseArticles.Add(article);
|
||||
await _context.SaveChangesAsync();
|
||||
return article;
|
||||
}
|
||||
|
||||
public async Task<KnowledgeBaseArticle?> UpdateArticleAsync(KnowledgeBaseArticle article)
|
||||
{
|
||||
var existing = await _context.KnowledgeBaseArticles.FindAsync(article.Id);
|
||||
if (existing == null) return null;
|
||||
|
||||
existing.Title = article.Title;
|
||||
existing.Content = article.Content;
|
||||
existing.CategoryId = article.CategoryId;
|
||||
existing.IsPublished = article.IsPublished;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteArticleAsync(Guid id)
|
||||
{
|
||||
var article = await _context.KnowledgeBaseArticles.FindAsync(id);
|
||||
if (article == null) return false;
|
||||
_context.KnowledgeBaseArticles.Remove(article);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Categories
|
||||
public async Task<KnowledgeBaseCategory?> GetCategoryByIdAsync(Guid id)
|
||||
=> await _context.KnowledgeBaseCategories
|
||||
.Include(c => c.Articles)
|
||||
.FirstOrDefaultAsync(c => c.Id == id);
|
||||
|
||||
public async Task<IEnumerable<KnowledgeBaseCategory>> GetAllCategoriesAsync()
|
||||
=> await _context.KnowledgeBaseCategories
|
||||
.Include(c => c.Articles)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.ThenBy(c => c.Name)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<KnowledgeBaseCategory> CreateCategoryAsync(KnowledgeBaseCategory category)
|
||||
{
|
||||
_context.KnowledgeBaseCategories.Add(category);
|
||||
await _context.SaveChangesAsync();
|
||||
return category;
|
||||
}
|
||||
|
||||
public async Task<KnowledgeBaseCategory?> UpdateCategoryAsync(KnowledgeBaseCategory category)
|
||||
{
|
||||
var existing = await _context.KnowledgeBaseCategories.FindAsync(category.Id);
|
||||
if (existing == null) return null;
|
||||
|
||||
existing.Name = category.Name;
|
||||
existing.Description = category.Description;
|
||||
existing.SortOrder = category.SortOrder;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteCategoryAsync(Guid id)
|
||||
{
|
||||
var category = await _context.KnowledgeBaseCategories.FindAsync(id);
|
||||
if (category == null) return false;
|
||||
_context.KnowledgeBaseCategories.Remove(category);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public interface ITicketCommentRepository
|
||||
{
|
||||
Task<TicketComment?> GetByIdAsync(Guid id);
|
||||
Task<IEnumerable<TicketComment>> GetByTicketIdAsync(Guid ticketId);
|
||||
Task<TicketComment> CreateAsync(TicketComment comment);
|
||||
Task<bool> DeleteAsync(Guid id);
|
||||
}
|
||||
|
||||
public interface IGroupRepository
|
||||
{
|
||||
Task<Group?> GetByIdAsync(Guid id);
|
||||
Task<IEnumerable<Group>> GetAllAsync();
|
||||
Task<Group> CreateAsync(Group group);
|
||||
Task<Group?> UpdateAsync(Group group);
|
||||
Task<bool> DeleteAsync(Guid id);
|
||||
}
|
||||
|
||||
public interface ISLARepository
|
||||
{
|
||||
Task<SLAPolicy?> GetByIdAsync(Guid id);
|
||||
Task<IEnumerable<SLAPolicy>> GetAllAsync();
|
||||
Task<SLAPolicy> CreateAsync(SLAPolicy policy);
|
||||
Task<SLAPolicy?> UpdateAsync(SLAPolicy policy);
|
||||
Task<bool> DeleteAsync(Guid id);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public class Repository<T> : IRepository<T> where T : class
|
||||
{
|
||||
protected readonly HelpDeskDbContext _context;
|
||||
protected readonly DbSet<T> _dbSet;
|
||||
|
||||
public Repository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
_dbSet = context.Set<T>();
|
||||
}
|
||||
|
||||
public virtual async Task<T?> GetByIdAsync(Guid id)
|
||||
{
|
||||
return await _dbSet.FindAsync(id);
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<T>> GetAllAsync()
|
||||
{
|
||||
return await _dbSet.ToListAsync();
|
||||
}
|
||||
|
||||
public virtual async Task<T> AddAsync(T entity)
|
||||
{
|
||||
await _dbSet.AddAsync(entity);
|
||||
await SaveChangesAsync();
|
||||
return entity;
|
||||
}
|
||||
|
||||
public virtual async Task UpdateAsync(T entity)
|
||||
{
|
||||
_dbSet.Update(entity);
|
||||
await SaveChangesAsync();
|
||||
}
|
||||
|
||||
public virtual async Task DeleteAsync(T entity)
|
||||
{
|
||||
_dbSet.Remove(entity);
|
||||
await SaveChangesAsync();
|
||||
}
|
||||
|
||||
public virtual async Task<int> SaveChangesAsync()
|
||||
{
|
||||
return await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public class TicketCommentRepository : ITicketCommentRepository
|
||||
{
|
||||
private readonly HelpDeskDbContext _context;
|
||||
|
||||
public TicketCommentRepository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<TicketComment?> GetByIdAsync(Guid id)
|
||||
=> await _context.TicketComments.FindAsync(id);
|
||||
|
||||
public async Task<IEnumerable<TicketComment>> GetByTicketIdAsync(Guid ticketId)
|
||||
=> await _context.TicketComments
|
||||
.Include(c => c.User)
|
||||
.Where(c => c.TicketId == ticketId)
|
||||
.OrderBy(c => c.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<TicketComment> CreateAsync(TicketComment comment)
|
||||
{
|
||||
_context.TicketComments.Add(comment);
|
||||
await _context.SaveChangesAsync();
|
||||
return comment;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
var comment = await _context.TicketComments.FindAsync(id);
|
||||
if (comment == null) return false;
|
||||
_context.TicketComments.Remove(comment);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class GroupRepository : IGroupRepository
|
||||
{
|
||||
private readonly HelpDeskDbContext _context;
|
||||
|
||||
public GroupRepository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Group?> GetByIdAsync(Guid id)
|
||||
=> await _context.Groups.FindAsync(id);
|
||||
|
||||
public async Task<IEnumerable<Group>> GetAllAsync()
|
||||
=> await _context.Groups.OrderBy(g => g.Name).ToListAsync();
|
||||
|
||||
public async Task<Group> CreateAsync(Group group)
|
||||
{
|
||||
_context.Groups.Add(group);
|
||||
await _context.SaveChangesAsync();
|
||||
return group;
|
||||
}
|
||||
|
||||
public async Task<Group?> UpdateAsync(Group group)
|
||||
{
|
||||
var existing = await _context.Groups.FindAsync(group.Id);
|
||||
if (existing == null) return null;
|
||||
|
||||
existing.Name = group.Name;
|
||||
existing.Description = group.Description;
|
||||
existing.IsActive = group.IsActive;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
var group = await _context.Groups.FindAsync(id);
|
||||
if (group == null) return false;
|
||||
_context.Groups.Remove(group);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class SLARepository : ISLARepository
|
||||
{
|
||||
private readonly HelpDeskDbContext _context;
|
||||
|
||||
public SLARepository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<SLAPolicy?> GetByIdAsync(Guid id)
|
||||
=> await _context.SLAPolicies.FindAsync(id);
|
||||
|
||||
public async Task<IEnumerable<SLAPolicy>> GetAllAsync()
|
||||
=> await _context.SLAPolicies.OrderBy(s => s.Name).ToListAsync();
|
||||
|
||||
public async Task<SLAPolicy> CreateAsync(SLAPolicy policy)
|
||||
{
|
||||
_context.SLAPolicies.Add(policy);
|
||||
await _context.SaveChangesAsync();
|
||||
return policy;
|
||||
}
|
||||
|
||||
public async Task<SLAPolicy?> UpdateAsync(SLAPolicy policy)
|
||||
{
|
||||
var existing = await _context.SLAPolicies.FindAsync(policy.Id);
|
||||
if (existing == null) return null;
|
||||
|
||||
existing.Name = policy.Name;
|
||||
existing.Description = policy.Description;
|
||||
existing.ResponseTimeHours = policy.ResponseTimeHours;
|
||||
existing.ResolutionTimeHours = policy.ResolutionTimeHours;
|
||||
existing.IsActive = policy.IsActive;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
var policy = await _context.SLAPolicies.FindAsync(id);
|
||||
if (policy == null) return false;
|
||||
_context.SLAPolicies.Remove(policy);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
using HelpDesk.Models.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public class TicketRepository : ITicketRepository
|
||||
{
|
||||
private readonly HelpDeskDbContext _context;
|
||||
|
||||
public TicketRepository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
private IQueryable<Ticket> GetQueryable()
|
||||
=> _context.Tickets
|
||||
.Include(t => t.Requester)
|
||||
.Include(t => t.Assignee)
|
||||
.Include(t => t.Group)
|
||||
.Include(t => t.SlaPolicy)
|
||||
.Include(t => t.Comments);
|
||||
|
||||
public async Task<Ticket?> GetByIdAsync(Guid id)
|
||||
=> await GetQueryable().FirstOrDefaultAsync(t => t.Id == id);
|
||||
|
||||
public async Task<IEnumerable<Ticket>> GetAllAsync(TicketStatus? status, TicketPriority? priority, Guid? assigneeId, Guid? requesterId, int page = 1, int pageSize = 20)
|
||||
{
|
||||
var query = GetQueryable().AsQueryable();
|
||||
|
||||
if (status.HasValue) query = query.Where(t => t.Status == status.Value);
|
||||
if (priority.HasValue) query = query.Where(t => t.Priority == priority.Value);
|
||||
if (assigneeId.HasValue) query = query.Where(t => t.AssigneeId == assigneeId.Value);
|
||||
if (requesterId.HasValue) query = query.Where(t => t.RequesterId == requesterId.Value);
|
||||
|
||||
return await query.OrderByDescending(t => t.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int> GetCountAsync(TicketStatus? status, TicketPriority? priority, Guid? assigneeId, Guid? requesterId)
|
||||
{
|
||||
var query = _context.Tickets.AsQueryable();
|
||||
|
||||
if (status.HasValue) query = query.Where(t => t.Status == status.Value);
|
||||
if (priority.HasValue) query = query.Where(t => t.Priority == priority.Value);
|
||||
if (assigneeId.HasValue) query = query.Where(t => t.AssigneeId == assigneeId.Value);
|
||||
if (requesterId.HasValue) query = query.Where(t => t.RequesterId == requesterId.Value);
|
||||
|
||||
return await query.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<Ticket> CreateAsync(Ticket ticket)
|
||||
{
|
||||
_context.Tickets.Add(ticket);
|
||||
await _context.SaveChangesAsync();
|
||||
return ticket;
|
||||
}
|
||||
|
||||
public async Task<Ticket?> UpdateAsync(Ticket ticket)
|
||||
{
|
||||
var existing = await _context.Tickets.FindAsync(ticket.Id);
|
||||
if (existing == null) return null;
|
||||
|
||||
existing.Title = ticket.Title;
|
||||
existing.Description = ticket.Description;
|
||||
existing.Priority = ticket.Priority;
|
||||
existing.Status = ticket.Status;
|
||||
existing.AssigneeId = ticket.AssigneeId;
|
||||
existing.GroupId = ticket.GroupId;
|
||||
existing.SlaPolicyId = ticket.SlaPolicyId;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
if (ticket.Status == TicketStatus.Resolved && existing.ResolvedAt == null)
|
||||
existing.ResolvedAt = DateTime.UtcNow;
|
||||
if (ticket.Status == TicketStatus.Closed && existing.ClosedAt == null)
|
||||
existing.ClosedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
var ticket = await _context.Tickets.FindAsync(id);
|
||||
if (ticket == null) return false;
|
||||
_context.Tickets.Remove(ticket);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalCountAsync()
|
||||
=> await _context.Tickets.CountAsync();
|
||||
|
||||
public async Task<int> GetCountByStatusAsync(TicketStatus status)
|
||||
=> await _context.Tickets.CountAsync(t => t.Status == status);
|
||||
|
||||
public async Task<int> GetCountByPriorityAsync(TicketPriority priority)
|
||||
=> await _context.Tickets.CountAsync(t => t.Priority == priority);
|
||||
|
||||
public async Task<double> GetAverageResponseTimeAsync()
|
||||
{
|
||||
var tickets = await _context.Tickets
|
||||
.Where(t => t.Comments.Any())
|
||||
.Select(t => new
|
||||
{
|
||||
t.CreatedAt,
|
||||
FirstCommentAt = t.Comments.OrderBy(c => c.CreatedAt).Select(c => c.CreatedAt).FirstOrDefault()
|
||||
})
|
||||
.Where(t => t.FirstCommentAt != default(DateTime))
|
||||
.ToListAsync();
|
||||
|
||||
if (!tickets.Any()) return 0;
|
||||
return tickets.Average(t => (t.FirstCommentAt - t.CreatedAt).TotalMinutes);
|
||||
}
|
||||
|
||||
public async Task<double> GetAverageResolutionTimeAsync()
|
||||
{
|
||||
var tickets = await _context.Tickets
|
||||
.Where(t => t.ResolvedAt.HasValue)
|
||||
.Select(t => new { t.CreatedAt, t.ResolvedAt })
|
||||
.ToListAsync();
|
||||
|
||||
if (!tickets.Any()) return 0;
|
||||
return tickets.Average(t => (t.ResolvedAt!.Value - t.CreatedAt).TotalMinutes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using HelpDesk.Models.Entities;
|
||||
using HelpDesk.Models.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HelpDesk.Data.Repositories;
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
{
|
||||
private readonly HelpDeskDbContext _context;
|
||||
|
||||
public UserRepository(HelpDeskDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<User?> GetByIdAsync(Guid id)
|
||||
=> await _context.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||||
|
||||
public async Task<User?> GetByEmailAsync(string email)
|
||||
=> await _context.Users.FirstOrDefaultAsync(u => u.Email == email);
|
||||
|
||||
public async Task<IEnumerable<User>> GetAllAsync()
|
||||
=> await _context.Users.OrderBy(u => u.LastName).ToListAsync();
|
||||
|
||||
public async Task<IEnumerable<User>> GetByRoleAsync(UserRole role)
|
||||
=> await _context.Users.Where(u => u.Role == role && u.IsActive).OrderBy(u => u.LastName).ToListAsync();
|
||||
|
||||
public async Task<User> CreateAsync(User user)
|
||||
{
|
||||
_context.Users.Add(user);
|
||||
await _context.SaveChangesAsync();
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<User?> UpdateAsync(User user)
|
||||
{
|
||||
var existing = await _context.Users.FindAsync(user.Id);
|
||||
if (existing == null) return null;
|
||||
|
||||
existing.FirstName = user.FirstName;
|
||||
existing.LastName = user.LastName;
|
||||
existing.Email = user.Email;
|
||||
existing.PhoneNumber = user.PhoneNumber;
|
||||
existing.AvatarUrl = user.AvatarUrl;
|
||||
existing.Role = user.Role;
|
||||
existing.IsActive = user.IsActive;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string email)
|
||||
=> await _context.Users.AnyAsync(u => u.Email == email);
|
||||
}
|
||||
Reference in new issue
Block a user