From 2249d3990596deca6402523546807059ae8dbb56 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Nov 2025 19:28:46 +0000 Subject: [PATCH 1/9] fix: Add missing Antd permissions to role and user seeders The AntdProjectsController requires Permissions.Antd.Projects permission, but this permission (along with all other Antd permissions) was defined in Constants/Permissions.cs but never assigned to any roles or users in the seeders. This caused 403 Forbidden errors when demo/normal users tried to access Antd endpoints like /api/v1/antd/projects, even though they could access other Antd endpoints that only required [Authorize] (like tasks). Changes: - Added all Permissions.Antd.* permissions to Admin role in PermissionUpdateSeeder - Added all Permissions.Antd.* permissions to User role in PermissionUpdateSeeder - Added all Permissions.Antd.* permissions to admin user in UserPermissionUpdateSeeder - Added all Permissions.Antd.* permissions to normal users in UserPermissionUpdateSeeder After restarting the application, the seeders will automatically update existing users with these new permissions. --- Data/Seeders/PermissionUpdateSeeder.cs | 48 ++++++++++++++++++++++ Data/Seeders/UserPermissionUpdateSeeder.cs | 48 ++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/Data/Seeders/PermissionUpdateSeeder.cs b/Data/Seeders/PermissionUpdateSeeder.cs index 6f15af0..fcf1f0f 100644 --- a/Data/Seeders/PermissionUpdateSeeder.cs +++ b/Data/Seeders/PermissionUpdateSeeder.cs @@ -37,6 +37,30 @@ public static async Task UpdateRolePermissionsAsync(IServiceProvider serviceProv Permissions.Personal.Invoices, Permissions.Personal.Files, Permissions.Personal.Chats, + + // Antd Dashboard permissions + Permissions.Antd.Projects, + Permissions.Antd.Clients, + Permissions.Antd.Products, + Permissions.Antd.Sellers, + Permissions.Antd.Orders, + Permissions.Antd.CampaignAds, + Permissions.Antd.SocialMediaStats, + Permissions.Antd.SocialMediaActivities, + Permissions.Antd.ScheduledPosts, + Permissions.Antd.LiveAuctions, + Permissions.Antd.AuctionCreators, + Permissions.Antd.BiddingTopSellers, + Permissions.Antd.BiddingTransactions, + Permissions.Antd.Courses, + Permissions.Antd.StudyStatistics, + Permissions.Antd.RecommendedCourses, + Permissions.Antd.Exams, + Permissions.Antd.CommunityGroups, + Permissions.Antd.TruckDeliveries, + Permissions.Antd.DeliveryAnalytics, + Permissions.Antd.Trucks, + Permissions.Antd.TruckDeliveryRequests, ] }, { @@ -55,6 +79,30 @@ public static async Task UpdateRolePermissionsAsync(IServiceProvider serviceProv Permissions.Personal.Invoices, Permissions.Personal.Files, Permissions.Personal.Chats, + + // Antd Dashboard permissions + Permissions.Antd.Projects, + Permissions.Antd.Clients, + Permissions.Antd.Products, + Permissions.Antd.Sellers, + Permissions.Antd.Orders, + Permissions.Antd.CampaignAds, + Permissions.Antd.SocialMediaStats, + Permissions.Antd.SocialMediaActivities, + Permissions.Antd.ScheduledPosts, + Permissions.Antd.LiveAuctions, + Permissions.Antd.AuctionCreators, + Permissions.Antd.BiddingTopSellers, + Permissions.Antd.BiddingTransactions, + Permissions.Antd.Courses, + Permissions.Antd.StudyStatistics, + Permissions.Antd.RecommendedCourses, + Permissions.Antd.Exams, + Permissions.Antd.CommunityGroups, + Permissions.Antd.TruckDeliveries, + Permissions.Antd.DeliveryAnalytics, + Permissions.Antd.Trucks, + Permissions.Antd.TruckDeliveryRequests, ] } }; diff --git a/Data/Seeders/UserPermissionUpdateSeeder.cs b/Data/Seeders/UserPermissionUpdateSeeder.cs index 5fcec90..3901fd2 100644 --- a/Data/Seeders/UserPermissionUpdateSeeder.cs +++ b/Data/Seeders/UserPermissionUpdateSeeder.cs @@ -39,6 +39,30 @@ public static async Task UpdateUserPermissionsAsync(IServiceProvider serviceProv new Claim(CustomClaimTypes.Permission, Permissions.Personal.Invoices), new Claim(CustomClaimTypes.Permission, Permissions.Personal.Files), new Claim(CustomClaimTypes.Permission, Permissions.Personal.Chats), + + // Antd Dashboard permissions + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Projects), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Clients), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Products), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Sellers), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Orders), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.CampaignAds), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.SocialMediaStats), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.SocialMediaActivities), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.ScheduledPosts), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.LiveAuctions), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.AuctionCreators), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.BiddingTopSellers), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.BiddingTransactions), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Courses), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.StudyStatistics), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.RecommendedCourses), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Exams), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.CommunityGroups), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.TruckDeliveries), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.DeliveryAnalytics), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Trucks), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.TruckDeliveryRequests), }; await UpdateUserPermissions(userManager, adminUser, adminPermissions, logger); @@ -64,6 +88,30 @@ public static async Task UpdateUserPermissionsAsync(IServiceProvider serviceProv new Claim(CustomClaimTypes.Permission, Permissions.Personal.Invoices), new Claim(CustomClaimTypes.Permission, Permissions.Personal.Files), new Claim(CustomClaimTypes.Permission, Permissions.Personal.Chats), + + // Antd Dashboard permissions + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Projects), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Clients), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Products), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Sellers), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Orders), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.CampaignAds), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.SocialMediaStats), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.SocialMediaActivities), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.ScheduledPosts), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.LiveAuctions), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.AuctionCreators), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.BiddingTopSellers), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.BiddingTransactions), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Courses), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.StudyStatistics), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.RecommendedCourses), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Exams), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.CommunityGroups), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.TruckDeliveries), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.DeliveryAnalytics), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Trucks), + new Claim(CustomClaimTypes.Permission, Permissions.Antd.TruckDeliveryRequests), }; await UpdateUserPermissions(userManager, user, userPermissions, logger); From 777666c622ae9558fe8584e7c14c529846d62260 Mon Sep 17 00:00:00 2001 From: Kelvin Kiprop Date: Thu, 20 Nov 2025 22:34:26 +0300 Subject: [PATCH 2/9] feat: add Antd.Tasks permissions and update TasksController with permission-based authorization --- Constants/Permissions.cs | 1 + .../Antd/{TasksController.cs => AntdTasksController.cs} | 7 +++++-- Data/Seeders/UserPermissionUpdateSeeder.cs | 1 + appsettings.Development.json | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) rename Controllers/Antd/{TasksController.cs => AntdTasksController.cs} (94%) diff --git a/Constants/Permissions.cs b/Constants/Permissions.cs index a6a998e..5247023 100644 --- a/Constants/Permissions.cs +++ b/Constants/Permissions.cs @@ -38,6 +38,7 @@ public static class Personal // Antd Dashboard permissions public static class Antd { + public const string Tasks = "Permissions.Antd.Tasks"; public const string Projects = "Permissions.Antd.Projects"; public const string Clients = "Permissions.Antd.Clients"; public const string Products = "Permissions.Antd.Products"; diff --git a/Controllers/Antd/TasksController.cs b/Controllers/Antd/AntdTasksController.cs similarity index 94% rename from Controllers/Antd/TasksController.cs rename to Controllers/Antd/AntdTasksController.cs index 38d6143..153a487 100644 --- a/Controllers/Antd/TasksController.cs +++ b/Controllers/Antd/AntdTasksController.cs @@ -1,5 +1,7 @@ +using AdminHubApi.Constants; using AdminHubApi.Dtos.Antd; using AdminHubApi.Interfaces.Antd; +using AdminHubApi.Security; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -8,11 +10,12 @@ namespace AdminHubApi.Controllers.Antd [Route("/api/v1/antd/tasks")] [Tags("Antd - Tasks")] [Authorize] - public class TasksController : AntdBaseController + [PermissionAuthorize(Permissions.Antd.Tasks)] + public class AntdTasksController : AntdBaseController { private readonly ITaskService _taskService; - public TasksController(ITaskService taskService, ILogger logger) + public AntdTasksController(ITaskService taskService, ILogger logger) : base(logger) { _taskService = taskService; diff --git a/Data/Seeders/UserPermissionUpdateSeeder.cs b/Data/Seeders/UserPermissionUpdateSeeder.cs index 3901fd2..b91c548 100644 --- a/Data/Seeders/UserPermissionUpdateSeeder.cs +++ b/Data/Seeders/UserPermissionUpdateSeeder.cs @@ -41,6 +41,7 @@ public static async Task UpdateUserPermissionsAsync(IServiceProvider serviceProv new Claim(CustomClaimTypes.Permission, Permissions.Personal.Chats), // Antd Dashboard permissions + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Tasks), new Claim(CustomClaimTypes.Permission, Permissions.Antd.Projects), new Claim(CustomClaimTypes.Permission, Permissions.Antd.Clients), new Claim(CustomClaimTypes.Permission, Permissions.Antd.Products), diff --git a/appsettings.Development.json b/appsettings.Development.json index f33aae7..c4571ad 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -28,7 +28,8 @@ "Cors": { "AllowedOrigins": [ "http://localhost:3000", - "http://localhost:3001" + "http://localhost:3001", + "http://localhost:5173" ] } } From 5420439ea9cd386c03ae3dc82bcec0e5d1d6a7ac Mon Sep 17 00:00:00 2001 From: Kelvin Kiprop Date: Thu, 20 Nov 2025 22:42:14 +0300 Subject: [PATCH 3/9] feat: add Antd.Tasks permission to UserPermissionUpdateSeeder --- Data/Seeders/UserPermissionUpdateSeeder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Data/Seeders/UserPermissionUpdateSeeder.cs b/Data/Seeders/UserPermissionUpdateSeeder.cs index b91c548..47c1f32 100644 --- a/Data/Seeders/UserPermissionUpdateSeeder.cs +++ b/Data/Seeders/UserPermissionUpdateSeeder.cs @@ -91,6 +91,7 @@ public static async Task UpdateUserPermissionsAsync(IServiceProvider serviceProv new Claim(CustomClaimTypes.Permission, Permissions.Personal.Chats), // Antd Dashboard permissions + new Claim(CustomClaimTypes.Permission, Permissions.Antd.Tasks), new Claim(CustomClaimTypes.Permission, Permissions.Antd.Projects), new Claim(CustomClaimTypes.Permission, Permissions.Antd.Clients), new Claim(CustomClaimTypes.Permission, Permissions.Antd.Products), From 1c3823a82ca676a9859dc2545c409895cc92d92a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 18:30:40 +0000 Subject: [PATCH 4/9] feat: Add Antd Employee endpoints with full CRUD operations - Add AntdEmployee entity with employee fields (name, role, age, email, country, salary, hire date) - Create AntdEmployeeDto with query params, statistics, and response types - Implement IAntdEmployeeService with GetAll, GetById, GetStatistics, Create, Update, Delete - Add AntdEmployeesController with REST endpoints - Configure entity indexes and precision in ApplicationDbContext - Add Permissions.Antd.Employees permission constant - Register AntdEmployeeService in Program.cs --- Constants/Permissions.cs | 1 + Controllers/Antd/AntdEmployeesController.cs | 123 ++++++++ Data/ApplicationDbContext.cs | 11 + Dtos/Antd/AntdEmployeeDto.cs | 104 +++++++ Entities/Antd/AntdEmployee.cs | 48 +++ Interfaces/Antd/IAntdEmployeeService.cs | 14 + Program.cs | 1 + Services/Antd/AntdEmployeeService.cs | 319 ++++++++++++++++++++ 8 files changed, 621 insertions(+) create mode 100644 Controllers/Antd/AntdEmployeesController.cs create mode 100644 Dtos/Antd/AntdEmployeeDto.cs create mode 100644 Entities/Antd/AntdEmployee.cs create mode 100644 Interfaces/Antd/IAntdEmployeeService.cs create mode 100644 Services/Antd/AntdEmployeeService.cs diff --git a/Constants/Permissions.cs b/Constants/Permissions.cs index a6a998e..0a8b946 100644 --- a/Constants/Permissions.cs +++ b/Constants/Permissions.cs @@ -60,6 +60,7 @@ public static class Antd public const string DeliveryAnalytics = "Permissions.Antd.DeliveryAnalytics"; public const string Trucks = "Permissions.Antd.Trucks"; public const string TruckDeliveryRequests = "Permissions.Antd.TruckDeliveryRequests"; + public const string Employees = "Permissions.Antd.Employees"; } diff --git a/Controllers/Antd/AntdEmployeesController.cs b/Controllers/Antd/AntdEmployeesController.cs new file mode 100644 index 0000000..b2c7138 --- /dev/null +++ b/Controllers/Antd/AntdEmployeesController.cs @@ -0,0 +1,123 @@ +using AdminHubApi.Constants; +using AdminHubApi.Dtos.Antd; +using AdminHubApi.Interfaces.Antd; +using AdminHubApi.Security; +using Microsoft.AspNetCore.Mvc; + +namespace AdminHubApi.Controllers.Antd +{ + [Route("/api/v1/antd/employees")] + [Tags("Antd - Employees")] + [PermissionAuthorize(Permissions.Antd.Employees)] + public class AntdEmployeesController : AntdBaseController + { + private readonly IAntdEmployeeService _employeeService; + + public AntdEmployeesController(IAntdEmployeeService employeeService, ILogger logger) + : base(logger) + { + _employeeService = employeeService; + } + + [HttpGet] + [ProducesResponseType(typeof(AntdEmployeeListResponse), 200)] + public async Task GetAllEmployees([FromQuery] AntdEmployeeQueryParams queryParams) + { + try + { + var response = await _employeeService.GetAllAsync(queryParams); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd employees"); + return ErrorResponse("Failed to retrieve employees", 500); + } + } + + [HttpGet("statistics")] + [ProducesResponseType(typeof(AntdEmployeeStatisticsResponse), 200)] + public async Task GetStatistics() + { + try + { + var response = await _employeeService.GetStatisticsAsync(); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd employee statistics"); + return ErrorResponse("Failed to retrieve statistics", 500); + } + } + + [HttpGet("{id}")] + [ProducesResponseType(typeof(AntdEmployeeResponse), 200)] + public async Task GetEmployeeById(string id) + { + try + { + var response = await _employeeService.GetByIdAsync(id); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd employee {EmployeeId}", id); + return ErrorResponse("Failed to retrieve employee", 500); + } + } + + [HttpPost] + [ProducesResponseType(typeof(AntdEmployeeCreateResponse), 201)] + public async Task CreateEmployee([FromBody] AntdEmployeeDto employeeDto) + { + try + { + if (!ModelState.IsValid) return BadRequest(ModelState); + var response = await _employeeService.CreateAsync(employeeDto); + return StatusCode(201, response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Antd employee"); + return ErrorResponse("Failed to create employee", 500); + } + } + + [HttpPut("{id}")] + [ProducesResponseType(typeof(AntdEmployeeUpdateResponse), 200)] + public async Task UpdateEmployee(string id, [FromBody] AntdEmployeeDto employeeDto) + { + try + { + if (!ModelState.IsValid) return BadRequest(ModelState); + var response = await _employeeService.UpdateAsync(id, employeeDto); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating Antd employee {EmployeeId}", id); + return ErrorResponse("Failed to update employee", 500); + } + } + + [HttpDelete("{id}")] + [ProducesResponseType(typeof(AntdEmployeeDeleteResponse), 200)] + public async Task DeleteEmployee(string id) + { + try + { + var response = await _employeeService.DeleteAsync(id); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting Antd employee {EmployeeId}", id); + return ErrorResponse("Failed to delete employee", 500); + } + } + } +} diff --git a/Data/ApplicationDbContext.cs b/Data/ApplicationDbContext.cs index de710ad..03d2cb5 100644 --- a/Data/ApplicationDbContext.cs +++ b/Data/ApplicationDbContext.cs @@ -58,6 +58,7 @@ public ApplicationDbContext(DbContextOptions options) : ba public DbSet AntdDeliveryAnalytics { get; set; } public DbSet AntdTrucks { get; set; } public DbSet AntdTruckDeliveryRequests { get; set; } + public DbSet AntdEmployees { get; set; } protected override void OnModelCreating(ModelBuilder builder) { @@ -403,6 +404,16 @@ protected override void OnModelCreating(ModelBuilder builder) entity.Property(e => e.CargoWeight).HasPrecision(18, 2); }); + builder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.Email); + entity.HasIndex(e => e.Role); + entity.HasIndex(e => e.Country); + entity.HasIndex(e => e.HireDate); + entity.Property(e => e.Salary).HasPrecision(18, 2); + }); + // Configure Product entity builder.Entity(entity => { diff --git a/Dtos/Antd/AntdEmployeeDto.cs b/Dtos/Antd/AntdEmployeeDto.cs new file mode 100644 index 0000000..555d146 --- /dev/null +++ b/Dtos/Antd/AntdEmployeeDto.cs @@ -0,0 +1,104 @@ +using AdminHubApi.Dtos.ApiResponse; +using System.Text.Json.Serialization; + +namespace AdminHubApi.Dtos.Antd +{ + public class AntdEmployeeDto + { + [JsonPropertyName("employee_id")] + public string EmployeeId { get; set; } = string.Empty; + + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + [JsonPropertyName("first_name")] + public string FirstName { get; set; } = string.Empty; + + [JsonPropertyName("middle_name")] + public string MiddleName { get; set; } = string.Empty; + + [JsonPropertyName("last_name")] + public string LastName { get; set; } = string.Empty; + + [JsonPropertyName("avatar")] + public string Avatar { get; set; } = string.Empty; + + [JsonPropertyName("role")] + public string Role { get; set; } = string.Empty; + + [JsonPropertyName("age")] + public int Age { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } = string.Empty; + + [JsonPropertyName("country")] + public string Country { get; set; } = string.Empty; + + [JsonPropertyName("favorite_color")] + public string FavoriteColor { get; set; } = string.Empty; + + [JsonPropertyName("hire_date")] + public string HireDate { get; set; } = string.Empty; + + [JsonPropertyName("salary")] + public decimal Salary { get; set; } + } + + public class AntdEmployeeQueryParams + { + public int Page { get; set; } = 1; + public int Limit { get; set; } = 20; + public string? Role { get; set; } + public string? Country { get; set; } + public int? MinAge { get; set; } + public int? MaxAge { get; set; } + public decimal? MinSalary { get; set; } + public decimal? MaxSalary { get; set; } + public string? SearchTerm { get; set; } + public string SortBy { get; set; } = "lastName"; + public string SortOrder { get; set; } = "asc"; + } + + public class AntdEmployeeStatisticsDto + { + [JsonPropertyName("total_employees")] + public int TotalEmployees { get; set; } + + [JsonPropertyName("average_salary")] + public decimal AverageSalary { get; set; } + + [JsonPropertyName("average_age")] + public double AverageAge { get; set; } + + [JsonPropertyName("employees_by_country")] + public Dictionary EmployeesByCountry { get; set; } = new(); + + [JsonPropertyName("employees_by_role")] + public Dictionary EmployeesByRole { get; set; } = new(); + } + + public class AntdEmployeeResponse : ApiResponse + { + } + + public class AntdEmployeeListResponse : ApiResponse> + { + } + + public class AntdEmployeeStatisticsResponse : ApiResponse + { + } + + public class AntdEmployeeCreateResponse : ApiResponse + { + } + + public class AntdEmployeeUpdateResponse : ApiResponse + { + } + + public class AntdEmployeeDeleteResponse : ApiResponse + { + } +} diff --git a/Entities/Antd/AntdEmployee.cs b/Entities/Antd/AntdEmployee.cs new file mode 100644 index 0000000..2e396d0 --- /dev/null +++ b/Entities/Antd/AntdEmployee.cs @@ -0,0 +1,48 @@ +using System.ComponentModel.DataAnnotations; + +namespace AdminHubApi.Entities.Antd +{ + public class AntdEmployee + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [MaxLength(50)] + public string Title { get; set; } = string.Empty; + + [Required] + [MaxLength(200)] + public string FirstName { get; set; } = string.Empty; + + [MaxLength(200)] + public string MiddleName { get; set; } = string.Empty; + + [Required] + [MaxLength(200)] + public string LastName { get; set; } = string.Empty; + + public string Avatar { get; set; } = string.Empty; + + [MaxLength(200)] + public string Role { get; set; } = string.Empty; + + public int Age { get; set; } + + [Required] + [MaxLength(255)] + public string Email { get; set; } = string.Empty; + + [MaxLength(100)] + public string Country { get; set; } = string.Empty; + + [MaxLength(50)] + public string FavoriteColor { get; set; } = string.Empty; + + public DateTime? HireDate { get; set; } + + public decimal Salary { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/Interfaces/Antd/IAntdEmployeeService.cs b/Interfaces/Antd/IAntdEmployeeService.cs new file mode 100644 index 0000000..8822d24 --- /dev/null +++ b/Interfaces/Antd/IAntdEmployeeService.cs @@ -0,0 +1,14 @@ +using AdminHubApi.Dtos.Antd; + +namespace AdminHubApi.Interfaces.Antd +{ + public interface IAntdEmployeeService + { + Task GetAllAsync(AntdEmployeeQueryParams queryParams); + Task GetByIdAsync(string id); + Task GetStatisticsAsync(); + Task CreateAsync(AntdEmployeeDto employeeDto); + Task UpdateAsync(string id, AntdEmployeeDto employeeDto); + Task DeleteAsync(string id); + } +} diff --git a/Program.cs b/Program.cs index 16b2cb5..973c962 100644 --- a/Program.cs +++ b/Program.cs @@ -195,6 +195,7 @@ await tokenBlacklistRepository.IsTokenBlacklistedAsync(tokenId)) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Repository builder.Services.AddScoped(); diff --git a/Services/Antd/AntdEmployeeService.cs b/Services/Antd/AntdEmployeeService.cs new file mode 100644 index 0000000..377d372 --- /dev/null +++ b/Services/Antd/AntdEmployeeService.cs @@ -0,0 +1,319 @@ +using AdminHubApi.Data; +using AdminHubApi.Dtos.ApiResponse; +using AdminHubApi.Dtos.Antd; +using AdminHubApi.Entities.Antd; +using AdminHubApi.Interfaces.Antd; +using Microsoft.EntityFrameworkCore; + +namespace AdminHubApi.Services.Antd +{ + public class AntdEmployeeService : IAntdEmployeeService + { + private readonly ApplicationDbContext _context; + private readonly ILogger _logger; + + public AntdEmployeeService(ApplicationDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task GetAllAsync(AntdEmployeeQueryParams queryParams) + { + try + { + var query = _context.AntdEmployees.AsQueryable(); + + // Apply filters + if (!string.IsNullOrEmpty(queryParams.Role)) + query = query.Where(e => e.Role.ToLower() == queryParams.Role.ToLower()); + + if (!string.IsNullOrEmpty(queryParams.Country)) + query = query.Where(e => e.Country.ToLower() == queryParams.Country.ToLower()); + + if (queryParams.MinAge.HasValue) + query = query.Where(e => e.Age >= queryParams.MinAge.Value); + + if (queryParams.MaxAge.HasValue) + query = query.Where(e => e.Age <= queryParams.MaxAge.Value); + + if (queryParams.MinSalary.HasValue) + query = query.Where(e => e.Salary >= queryParams.MinSalary.Value); + + if (queryParams.MaxSalary.HasValue) + query = query.Where(e => e.Salary <= queryParams.MaxSalary.Value); + + if (!string.IsNullOrEmpty(queryParams.SearchTerm)) + { + var searchLower = queryParams.SearchTerm.ToLower(); + query = query.Where(e => + e.FirstName.ToLower().Contains(searchLower) || + e.LastName.ToLower().Contains(searchLower) || + e.Email.ToLower().Contains(searchLower) || + e.Role.ToLower().Contains(searchLower)); + } + + // Apply sorting + query = queryParams.SortBy.ToLower() switch + { + "firstname" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(e => e.FirstName) + : query.OrderBy(e => e.FirstName), + "lastname" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(e => e.LastName) + : query.OrderBy(e => e.LastName), + "age" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(e => e.Age) + : query.OrderBy(e => e.Age), + "salary" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(e => e.Salary) + : query.OrderBy(e => e.Salary), + "hiredate" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(e => e.HireDate) + : query.OrderBy(e => e.HireDate), + "role" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(e => e.Role) + : query.OrderBy(e => e.Role), + "country" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(e => e.Country) + : query.OrderBy(e => e.Country), + _ => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(e => e.LastName) + : query.OrderBy(e => e.LastName) + }; + + var total = await query.CountAsync(); + var employees = await query + .Skip((queryParams.Page - 1) * queryParams.Limit) + .Take(queryParams.Limit) + .ToListAsync(); + + return new AntdEmployeeListResponse + { + Succeeded = true, + Data = employees.Select(MapToDto).ToList(), + Message = "Employees retrieved successfully", + Meta = new PaginationMeta + { + Page = queryParams.Page, + Limit = queryParams.Limit, + Total = total, + TotalPages = (int)Math.Ceiling((double)total / queryParams.Limit) + } + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd employees"); + throw; + } + } + + public async Task GetByIdAsync(string id) + { + try + { + if (!Guid.TryParse(id, out var guidId)) + return new AntdEmployeeResponse { Succeeded = false, Message = "Invalid employee ID format" }; + + var employee = await _context.AntdEmployees.FindAsync(guidId); + if (employee == null) + return new AntdEmployeeResponse { Succeeded = false, Message = "Employee not found" }; + + return new AntdEmployeeResponse + { + Succeeded = true, + Data = MapToDto(employee), + Message = "Employee retrieved successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd employee {EmployeeId}", id); + throw; + } + } + + public async Task GetStatisticsAsync() + { + try + { + var employees = await _context.AntdEmployees.ToListAsync(); + + if (employees.Count == 0) + { + return new AntdEmployeeStatisticsResponse + { + Succeeded = true, + Data = new AntdEmployeeStatisticsDto + { + TotalEmployees = 0, + AverageSalary = 0, + AverageAge = 0, + EmployeesByCountry = new Dictionary(), + EmployeesByRole = new Dictionary() + }, + Message = "Statistics retrieved successfully" + }; + } + + var statistics = new AntdEmployeeStatisticsDto + { + TotalEmployees = employees.Count, + AverageSalary = employees.Average(e => e.Salary), + AverageAge = employees.Average(e => e.Age), + EmployeesByCountry = employees + .GroupBy(e => e.Country) + .ToDictionary(g => g.Key, g => g.Count()), + EmployeesByRole = employees + .GroupBy(e => e.Role) + .ToDictionary(g => g.Key, g => g.Count()) + }; + + return new AntdEmployeeStatisticsResponse + { + Succeeded = true, + Data = statistics, + Message = "Statistics retrieved successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd employee statistics"); + throw; + } + } + + public async Task CreateAsync(AntdEmployeeDto employeeDto) + { + try + { + var employee = new AntdEmployee + { + Id = Guid.NewGuid(), + Title = employeeDto.Title, + FirstName = employeeDto.FirstName, + MiddleName = employeeDto.MiddleName, + LastName = employeeDto.LastName, + Avatar = employeeDto.Avatar, + Role = employeeDto.Role, + Age = employeeDto.Age, + Email = employeeDto.Email, + Country = employeeDto.Country, + FavoriteColor = employeeDto.FavoriteColor, + HireDate = string.IsNullOrEmpty(employeeDto.HireDate) + ? null + : DateTime.Parse(employeeDto.HireDate).ToUniversalTime(), + Salary = employeeDto.Salary, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + _context.AntdEmployees.Add(employee); + await _context.SaveChangesAsync(); + + return new AntdEmployeeCreateResponse + { + Succeeded = true, + Data = MapToDto(employee), + Message = "Employee created successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Antd employee"); + throw; + } + } + + public async Task UpdateAsync(string id, AntdEmployeeDto employeeDto) + { + try + { + if (!Guid.TryParse(id, out var guidId)) + return new AntdEmployeeUpdateResponse { Succeeded = false, Message = "Invalid employee ID format" }; + + var employee = await _context.AntdEmployees.FindAsync(guidId); + if (employee == null) + return new AntdEmployeeUpdateResponse { Succeeded = false, Message = "Employee not found" }; + + employee.Title = employeeDto.Title; + employee.FirstName = employeeDto.FirstName; + employee.MiddleName = employeeDto.MiddleName; + employee.LastName = employeeDto.LastName; + employee.Avatar = employeeDto.Avatar; + employee.Role = employeeDto.Role; + employee.Age = employeeDto.Age; + employee.Email = employeeDto.Email; + employee.Country = employeeDto.Country; + employee.FavoriteColor = employeeDto.FavoriteColor; + employee.HireDate = string.IsNullOrEmpty(employeeDto.HireDate) + ? null + : DateTime.Parse(employeeDto.HireDate).ToUniversalTime(); + employee.Salary = employeeDto.Salary; + employee.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + return new AntdEmployeeUpdateResponse + { + Succeeded = true, + Data = MapToDto(employee), + Message = "Employee updated successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating Antd employee {EmployeeId}", id); + throw; + } + } + + public async Task DeleteAsync(string id) + { + try + { + if (!Guid.TryParse(id, out var guidId)) + return new AntdEmployeeDeleteResponse { Succeeded = false, Message = "Invalid employee ID format" }; + + var employee = await _context.AntdEmployees.FindAsync(guidId); + if (employee == null) + return new AntdEmployeeDeleteResponse { Succeeded = false, Message = "Employee not found" }; + + _context.AntdEmployees.Remove(employee); + await _context.SaveChangesAsync(); + + return new AntdEmployeeDeleteResponse + { + Succeeded = true, + Message = "Employee deleted successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting Antd employee {EmployeeId}", id); + throw; + } + } + + private static AntdEmployeeDto MapToDto(AntdEmployee employee) + { + return new AntdEmployeeDto + { + EmployeeId = employee.Id.ToString(), + Title = employee.Title, + FirstName = employee.FirstName, + MiddleName = employee.MiddleName, + LastName = employee.LastName, + Avatar = employee.Avatar, + Role = employee.Role, + Age = employee.Age, + Email = employee.Email, + Country = employee.Country, + FavoriteColor = employee.FavoriteColor, + HireDate = employee.HireDate?.ToString("M/d/yyyy") ?? string.Empty, + Salary = employee.Salary + }; + } + } +} From 452f1505637cdc207505472491c21fc9ac43f39f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 18:35:03 +0000 Subject: [PATCH 5/9] feat: Add seed data for Antd Employees - Add 10 sample employee records to AntdDataSeeder - Includes diverse roles, countries, and salary ranges - Seeds data on application startup if table is empty --- Data/Seeders/AntdDataSeeder.cs | 172 +++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/Data/Seeders/AntdDataSeeder.cs b/Data/Seeders/AntdDataSeeder.cs index d2d26a2..7fba8cc 100644 --- a/Data/Seeders/AntdDataSeeder.cs +++ b/Data/Seeders/AntdDataSeeder.cs @@ -775,6 +775,178 @@ public static async Task SeedAntdDataAsync(IServiceProvider serviceProvider) await context.SaveChangesAsync(); logger.LogInformation("Antd truck delivery requests data seeded successfully"); } + + // Seed Antd Employees (10 rows) + if (!await context.AntdEmployees.AnyAsync()) + { + logger.LogInformation("Seeding Antd employees data..."); + var employees = new List + { + new AntdEmployee + { + Id = Guid.Parse("24e4e64c-bf09-459f-8cea-f9d2de99d15b"), + Title = "Mrs", + FirstName = "Eugen", + MiddleName = "Pål", + LastName = "Tiltman", + Avatar = "https://images.unsplash.com/photo-1633332755192-727a05c4013d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8dXNlcnxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=400&q=60", + Role = "Operator", + Age = 28, + Email = "etiltman0@dailymail.co.uk", + Country = "Indonesia", + FavoriteColor = "gray", + HireDate = new DateTime(2017, 4, 9, 0, 0, 0, DateTimeKind.Utc), + Salary = 92877.67m + }, + new AntdEmployee + { + Id = Guid.Parse("3a33bd87-072b-46f0-847a-0f767bc34dcf"), + Title = "Ms", + FirstName = "Anna-diane", + MiddleName = "Maëlla", + LastName = "O'Hoey", + Avatar = "https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8dXNlcnxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=400&q=60", + Role = "Nurse Practicioner", + Age = 53, + Email = "aohoey1@eventbrite.com", + Country = "China", + FavoriteColor = "maroon", + HireDate = new DateTime(2010, 9, 27, 0, 0, 0, DateTimeKind.Utc), + Salary = 32804.83m + }, + new AntdEmployee + { + Id = Guid.NewGuid(), + Title = "Mr", + FirstName = "James", + MiddleName = "Michael", + LastName = "Anderson", + Avatar = "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8dXNlcnxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=400&q=60", + Role = "Software Engineer", + Age = 32, + Email = "janderson@techcorp.com", + Country = "United States", + FavoriteColor = "blue", + HireDate = new DateTime(2019, 3, 15, 0, 0, 0, DateTimeKind.Utc), + Salary = 125000.00m + }, + new AntdEmployee + { + Id = Guid.NewGuid(), + Title = "Dr", + FirstName = "Sarah", + MiddleName = "Elizabeth", + LastName = "Martinez", + Avatar = "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Nnx8dXNlcnxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=400&q=60", + Role = "Medical Director", + Age = 45, + Email = "smartinez@healthcare.com", + Country = "Spain", + FavoriteColor = "green", + HireDate = new DateTime(2015, 7, 22, 0, 0, 0, DateTimeKind.Utc), + Salary = 185000.00m + }, + new AntdEmployee + { + Id = Guid.NewGuid(), + Title = "Mr", + FirstName = "David", + MiddleName = "John", + LastName = "Chen", + Avatar = "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8cG9ydHJhaXR8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=400&q=60", + Role = "Product Manager", + Age = 38, + Email = "dchen@product.io", + Country = "Singapore", + FavoriteColor = "red", + HireDate = new DateTime(2018, 1, 10, 0, 0, 0, DateTimeKind.Utc), + Salary = 142000.00m + }, + new AntdEmployee + { + Id = Guid.NewGuid(), + Title = "Ms", + FirstName = "Emma", + MiddleName = "Rose", + LastName = "Thompson", + Avatar = "https://images.unsplash.com/photo-1534528741775-53994a69daeb?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8OHx8cG9ydHJhaXR8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=400&q=60", + Role = "HR Manager", + Age = 41, + Email = "ethompson@company.com", + Country = "Canada", + FavoriteColor = "purple", + HireDate = new DateTime(2016, 5, 8, 0, 0, 0, DateTimeKind.Utc), + Salary = 98000.00m + }, + new AntdEmployee + { + Id = Guid.NewGuid(), + Title = "Mr", + FirstName = "Mohammed", + MiddleName = "Ali", + LastName = "Hassan", + Avatar = "https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NXx8cG9ydHJhaXR8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=400&q=60", + Role = "Marketing Specialist", + Age = 29, + Email = "mhassan@marketing.com", + Country = "United Arab Emirates", + FavoriteColor = "orange", + HireDate = new DateTime(2020, 11, 3, 0, 0, 0, DateTimeKind.Utc), + Salary = 78000.00m + }, + new AntdEmployee + { + Id = Guid.NewGuid(), + Title = "Mrs", + FirstName = "Lisa", + MiddleName = "Marie", + LastName = "Schmidt", + Avatar = "https://images.unsplash.com/photo-1544005313-94ddf0286df2?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8cG9ydHJhaXR8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=400&q=60", + Role = "Financial Analyst", + Age = 35, + Email = "lschmidt@finance.de", + Country = "Germany", + FavoriteColor = "yellow", + HireDate = new DateTime(2017, 9, 14, 0, 0, 0, DateTimeKind.Utc), + Salary = 95000.00m + }, + new AntdEmployee + { + Id = Guid.NewGuid(), + Title = "Mr", + FirstName = "Raj", + MiddleName = "Kumar", + LastName = "Patel", + Avatar = "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8N3x8cG9ydHJhaXR8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=400&q=60", + Role = "Data Scientist", + Age = 31, + Email = "rpatel@datascience.in", + Country = "India", + FavoriteColor = "teal", + HireDate = new DateTime(2019, 6, 20, 0, 0, 0, DateTimeKind.Utc), + Salary = 112000.00m + }, + new AntdEmployee + { + Id = Guid.NewGuid(), + Title = "Ms", + FirstName = "Yuki", + MiddleName = "Sakura", + LastName = "Tanaka", + Avatar = "https://images.unsplash.com/photo-1580489944761-15a19d654956?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8OXx8cG9ydHJhaXR8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=400&q=60", + Role = "UX Designer", + Age = 27, + Email = "ytanaka@design.jp", + Country = "Japan", + FavoriteColor = "pink", + HireDate = new DateTime(2021, 2, 18, 0, 0, 0, DateTimeKind.Utc), + Salary = 88000.00m + } + }; + context.AntdEmployees.AddRange(employees); + await context.SaveChangesAsync(); + logger.LogInformation("Antd employees data seeded successfully"); + } } catch (Exception ex) { From 82ff0e03908c6b474b4ee0b3c504c4bff8c059d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 18:45:34 +0000 Subject: [PATCH 6/9] feat: Add Antd FAQ endpoints with full CRUD operations and seed data - Add AntdFaq entity with question, answer, category, rating, views, tags - Create AntdFaqDto with query params, statistics, and response types - Implement IAntdFaqService with GetAll, GetById, GetFeatured, GetStatistics, Create, Update, Delete - Add AntdFaqsController with REST endpoints including featured FAQs - Configure entity indexes and precision in ApplicationDbContext - Add Permissions.Antd.Faqs permission constant - Register AntdFaqService in Program.cs - Add 10 sample FAQ records to AntdDataSeeder covering Shipping, Payment, Returns, Support, Orders, Product, Pricing, and Security categories --- Constants/Permissions.cs | 1 + Controllers/Antd/AntdFaqsController.cs | 139 +++++++++++ Data/ApplicationDbContext.cs | 10 + Data/Seeders/AntdDataSeeder.cs | 142 +++++++++++ Dtos/Antd/AntdFaqDto.cs | 92 +++++++ Entities/Antd/AntdFaq.cs | 35 +++ Interfaces/Antd/IAntdFaqService.cs | 15 ++ Program.cs | 1 + Services/Antd/AntdFaqService.cs | 320 +++++++++++++++++++++++++ 9 files changed, 755 insertions(+) create mode 100644 Controllers/Antd/AntdFaqsController.cs create mode 100644 Dtos/Antd/AntdFaqDto.cs create mode 100644 Entities/Antd/AntdFaq.cs create mode 100644 Interfaces/Antd/IAntdFaqService.cs create mode 100644 Services/Antd/AntdFaqService.cs diff --git a/Constants/Permissions.cs b/Constants/Permissions.cs index 0a8b946..e5f04cf 100644 --- a/Constants/Permissions.cs +++ b/Constants/Permissions.cs @@ -61,6 +61,7 @@ public static class Antd public const string Trucks = "Permissions.Antd.Trucks"; public const string TruckDeliveryRequests = "Permissions.Antd.TruckDeliveryRequests"; public const string Employees = "Permissions.Antd.Employees"; + public const string Faqs = "Permissions.Antd.Faqs"; } diff --git a/Controllers/Antd/AntdFaqsController.cs b/Controllers/Antd/AntdFaqsController.cs new file mode 100644 index 0000000..444ee18 --- /dev/null +++ b/Controllers/Antd/AntdFaqsController.cs @@ -0,0 +1,139 @@ +using AdminHubApi.Constants; +using AdminHubApi.Dtos.Antd; +using AdminHubApi.Interfaces.Antd; +using AdminHubApi.Security; +using Microsoft.AspNetCore.Mvc; + +namespace AdminHubApi.Controllers.Antd +{ + [Route("/api/v1/antd/faqs")] + [Tags("Antd - FAQs")] + [PermissionAuthorize(Permissions.Antd.Faqs)] + public class AntdFaqsController : AntdBaseController + { + private readonly IAntdFaqService _faqService; + + public AntdFaqsController(IAntdFaqService faqService, ILogger logger) + : base(logger) + { + _faqService = faqService; + } + + [HttpGet] + [ProducesResponseType(typeof(AntdFaqListResponse), 200)] + public async Task GetAllFaqs([FromQuery] AntdFaqQueryParams queryParams) + { + try + { + var response = await _faqService.GetAllAsync(queryParams); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd FAQs"); + return ErrorResponse("Failed to retrieve FAQs", 500); + } + } + + [HttpGet("featured")] + [ProducesResponseType(typeof(AntdFaqListResponse), 200)] + public async Task GetFeaturedFaqs([FromQuery] int limit = 10) + { + try + { + var response = await _faqService.GetFeaturedAsync(limit); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving featured Antd FAQs"); + return ErrorResponse("Failed to retrieve featured FAQs", 500); + } + } + + [HttpGet("statistics")] + [ProducesResponseType(typeof(AntdFaqStatisticsResponse), 200)] + public async Task GetStatistics() + { + try + { + var response = await _faqService.GetStatisticsAsync(); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd FAQ statistics"); + return ErrorResponse("Failed to retrieve statistics", 500); + } + } + + [HttpGet("{id}")] + [ProducesResponseType(typeof(AntdFaqResponse), 200)] + public async Task GetFaqById(string id) + { + try + { + var response = await _faqService.GetByIdAsync(id); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd FAQ {FaqId}", id); + return ErrorResponse("Failed to retrieve FAQ", 500); + } + } + + [HttpPost] + [ProducesResponseType(typeof(AntdFaqCreateResponse), 201)] + public async Task CreateFaq([FromBody] AntdFaqDto faqDto) + { + try + { + if (!ModelState.IsValid) return BadRequest(ModelState); + var response = await _faqService.CreateAsync(faqDto); + return StatusCode(201, response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Antd FAQ"); + return ErrorResponse("Failed to create FAQ", 500); + } + } + + [HttpPut("{id}")] + [ProducesResponseType(typeof(AntdFaqUpdateResponse), 200)] + public async Task UpdateFaq(string id, [FromBody] AntdFaqDto faqDto) + { + try + { + if (!ModelState.IsValid) return BadRequest(ModelState); + var response = await _faqService.UpdateAsync(id, faqDto); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating Antd FAQ {FaqId}", id); + return ErrorResponse("Failed to update FAQ", 500); + } + } + + [HttpDelete("{id}")] + [ProducesResponseType(typeof(AntdFaqDeleteResponse), 200)] + public async Task DeleteFaq(string id) + { + try + { + var response = await _faqService.DeleteAsync(id); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting Antd FAQ {FaqId}", id); + return ErrorResponse("Failed to delete FAQ", 500); + } + } + } +} diff --git a/Data/ApplicationDbContext.cs b/Data/ApplicationDbContext.cs index 03d2cb5..c712c36 100644 --- a/Data/ApplicationDbContext.cs +++ b/Data/ApplicationDbContext.cs @@ -59,6 +59,7 @@ public ApplicationDbContext(DbContextOptions options) : ba public DbSet AntdTrucks { get; set; } public DbSet AntdTruckDeliveryRequests { get; set; } public DbSet AntdEmployees { get; set; } + public DbSet AntdFaqs { get; set; } protected override void OnModelCreating(ModelBuilder builder) { @@ -414,6 +415,15 @@ protected override void OnModelCreating(ModelBuilder builder) entity.Property(e => e.Salary).HasPrecision(18, 2); }); + builder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.Category); + entity.HasIndex(e => e.IsFeatured); + entity.HasIndex(e => e.DateCreated); + entity.Property(e => e.Rating).HasPrecision(3, 1); + }); + // Configure Product entity builder.Entity(entity => { diff --git a/Data/Seeders/AntdDataSeeder.cs b/Data/Seeders/AntdDataSeeder.cs index 7fba8cc..f885a61 100644 --- a/Data/Seeders/AntdDataSeeder.cs +++ b/Data/Seeders/AntdDataSeeder.cs @@ -947,6 +947,148 @@ public static async Task SeedAntdDataAsync(IServiceProvider serviceProvider) await context.SaveChangesAsync(); logger.LogInformation("Antd employees data seeded successfully"); } + + // Seed Antd FAQs (10 rows) + if (!await context.AntdFaqs.AnyAsync()) + { + logger.LogInformation("Seeding Antd FAQs data..."); + var faqs = new List + { + new AntdFaq + { + Id = Guid.Parse("e57b6904-1e99-45c0-8eaf-ecfc28346ab1"), + Question = "How do I track my order?", + Answer = "You can track your order by logging into your account and navigating to the 'Orders' section. There you'll find real-time tracking information for all your orders.\n\nAlternatively, you can use the tracking number sent to your email to check the status on our tracking page.\n\nIf you're having trouble, our customer support team is available 24/7 to assist you.", + Category = "Shipping", + DateCreated = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc), + IsFeatured = true, + Views = 23340, + Tags = "tracking, orders, shipping, delivery", + Rating = 4.5m, + Author = "Erastus Hanssmann" + }, + new AntdFaq + { + Id = Guid.Parse("8f31ae56-0196-4dee-a3d6-3fcc53ea94c5"), + Question = "What payment methods do you accept?", + Answer = "We accept all major credit cards including Visa, Mastercard, American Express, and Discover.\n\nAdditionally, we support PayPal, Apple Pay, Google Pay, and bank transfers for larger orders.\n\nAll transactions are secured with industry-standard encryption to protect your financial information.\n\nFor enterprise customers, we also offer invoice-based payment terms.", + Category = "Payment", + DateCreated = new DateTime(2020, 8, 2, 0, 0, 0, DateTimeKind.Utc), + IsFeatured = false, + Views = 59358, + Tags = "payment, credit card, paypal, methods", + Rating = 4.8m, + Author = "Cly Moohan" + }, + new AntdFaq + { + Id = Guid.NewGuid(), + Question = "How long does shipping take?", + Answer = "Standard shipping typically takes 5-7 business days within the continental United States.\n\nExpress shipping is available and takes 2-3 business days.\n\nOvernight shipping is also available for urgent orders.\n\nInternational shipping times vary by destination, typically ranging from 7-21 business days.", + Category = "Shipping", + DateCreated = new DateTime(2021, 3, 15, 0, 0, 0, DateTimeKind.Utc), + IsFeatured = true, + Views = 45620, + Tags = "shipping, delivery, time, duration", + Rating = 4.2m, + Author = "Maria Garcia" + }, + new AntdFaq + { + Id = Guid.NewGuid(), + Question = "What is your return policy?", + Answer = "We offer a 30-day return policy for most items.\n\nProducts must be unused, in original packaging, and in resalable condition.\n\nTo initiate a return, log into your account and select the order you wish to return.\n\nRefunds are typically processed within 5-7 business days after we receive the returned item.\n\nShipping costs are non-refundable unless the return is due to our error.", + Category = "Returns", + DateCreated = new DateTime(2020, 5, 10, 0, 0, 0, DateTimeKind.Utc), + IsFeatured = true, + Views = 67890, + Tags = "returns, refund, policy, exchange", + Rating = 4.6m, + Author = "John Smith" + }, + new AntdFaq + { + Id = Guid.NewGuid(), + Question = "Do you ship internationally?", + Answer = "Yes, we ship to over 150 countries worldwide.\n\nInternational shipping rates and delivery times vary by destination.\n\nCustomers are responsible for any customs duties, taxes, or fees imposed by their country.\n\nSome items may have shipping restrictions based on local regulations.", + Category = "Shipping", + DateCreated = new DateTime(2021, 1, 20, 0, 0, 0, DateTimeKind.Utc), + IsFeatured = false, + Views = 34210, + Tags = "international, shipping, worldwide, global", + Rating = 4.3m, + Author = "Sarah Johnson" + }, + new AntdFaq + { + Id = Guid.NewGuid(), + Question = "How can I contact customer support?", + Answer = "Our customer support team is available 24/7 through multiple channels:\n\nEmail: support@example.com\nPhone: 1-800-555-0123\nLive Chat: Available on our website\nSocial Media: Facebook, Twitter, Instagram\n\nTypical response time is within 2-4 hours for emails and instant for live chat.", + Category = "Support", + DateCreated = new DateTime(2020, 11, 5, 0, 0, 0, DateTimeKind.Utc), + IsFeatured = true, + Views = 52340, + Tags = "support, contact, help, customer service", + Rating = 4.9m, + Author = "Michael Brown" + }, + new AntdFaq + { + Id = Guid.NewGuid(), + Question = "Can I change or cancel my order?", + Answer = "Orders can be modified or cancelled within 1 hour of placement.\n\nAfter this window, orders enter our fulfillment process and cannot be changed.\n\nTo request a change or cancellation, contact customer support immediately with your order number.\n\nIf your order has already shipped, you'll need to wait for delivery and process a return.", + Category = "Orders", + DateCreated = new DateTime(2021, 6, 12, 0, 0, 0, DateTimeKind.Utc), + IsFeatured = false, + Views = 28750, + Tags = "cancel, change, modify, order", + Rating = 4.1m, + Author = "Emily Davis" + }, + new AntdFaq + { + Id = Guid.NewGuid(), + Question = "Are there any warranties on products?", + Answer = "Most products come with a manufacturer's warranty ranging from 90 days to 2 years.\n\nWarranty details are listed on each product page.\n\nWe also offer extended warranty options for eligible products.\n\nWarranty claims should be submitted through our customer support portal with proof of purchase.", + Category = "Product", + DateCreated = new DateTime(2020, 9, 18, 0, 0, 0, DateTimeKind.Utc), + IsFeatured = false, + Views = 19630, + Tags = "warranty, guarantee, protection, coverage", + Rating = 4.4m, + Author = "David Wilson" + }, + new AntdFaq + { + Id = Guid.NewGuid(), + Question = "Do you offer bulk or wholesale pricing?", + Answer = "Yes, we offer special pricing for bulk orders and wholesale customers.\n\nBulk discounts start at orders of 50 units or more.\n\nWholesale accounts receive additional benefits including dedicated account managers and priority support.\n\nContact our business sales team at business@example.com for more information.", + Category = "Pricing", + DateCreated = new DateTime(2021, 4, 7, 0, 0, 0, DateTimeKind.Utc), + IsFeatured = false, + Views = 15420, + Tags = "bulk, wholesale, discount, pricing", + Rating = 4.7m, + Author = "Robert Taylor" + }, + new AntdFaq + { + Id = Guid.NewGuid(), + Question = "How secure is my personal information?", + Answer = "We take data security very seriously and implement industry-leading security measures.\n\nAll personal information is encrypted using SSL/TLS protocols.\n\nPayment information is processed through PCI-DSS compliant payment processors.\n\nWe never store complete credit card information on our servers.\n\nOur privacy policy details how we collect, use, and protect your data.", + Category = "Security", + DateCreated = new DateTime(2020, 7, 25, 0, 0, 0, DateTimeKind.Utc), + IsFeatured = true, + Views = 41280, + Tags = "security, privacy, data, protection", + Rating = 4.9m, + Author = "Jennifer Martinez" + } + }; + context.AntdFaqs.AddRange(faqs); + await context.SaveChangesAsync(); + logger.LogInformation("Antd FAQs data seeded successfully"); + } } catch (Exception ex) { diff --git a/Dtos/Antd/AntdFaqDto.cs b/Dtos/Antd/AntdFaqDto.cs new file mode 100644 index 0000000..ac9066c --- /dev/null +++ b/Dtos/Antd/AntdFaqDto.cs @@ -0,0 +1,92 @@ +using AdminHubApi.Dtos.ApiResponse; +using System.Text.Json.Serialization; + +namespace AdminHubApi.Dtos.Antd +{ + public class AntdFaqDto + { + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("question")] + public string Question { get; set; } = string.Empty; + + [JsonPropertyName("answer")] + public string Answer { get; set; } = string.Empty; + + [JsonPropertyName("category")] + public string Category { get; set; } = string.Empty; + + [JsonPropertyName("date_created")] + public string DateCreated { get; set; } = string.Empty; + + [JsonPropertyName("is_featured")] + public bool IsFeatured { get; set; } + + [JsonPropertyName("views")] + public int Views { get; set; } + + [JsonPropertyName("tags")] + public string Tags { get; set; } = string.Empty; + + [JsonPropertyName("rating")] + public decimal Rating { get; set; } + + [JsonPropertyName("author")] + public string Author { get; set; } = string.Empty; + } + + public class AntdFaqQueryParams + { + public int Page { get; set; } = 1; + public int Limit { get; set; } = 20; + public string? Category { get; set; } + public bool? IsFeatured { get; set; } + public decimal? MinRating { get; set; } + public string? SearchTerm { get; set; } + public string SortBy { get; set; } = "views"; + public string SortOrder { get; set; } = "desc"; + } + + public class AntdFaqStatisticsDto + { + [JsonPropertyName("total_faqs")] + public int TotalFaqs { get; set; } + + [JsonPropertyName("total_views")] + public int TotalViews { get; set; } + + [JsonPropertyName("average_rating")] + public decimal AverageRating { get; set; } + + [JsonPropertyName("featured_count")] + public int FeaturedCount { get; set; } + + [JsonPropertyName("faqs_by_category")] + public Dictionary FaqsByCategory { get; set; } = new(); + } + + public class AntdFaqResponse : ApiResponse + { + } + + public class AntdFaqListResponse : ApiResponse> + { + } + + public class AntdFaqStatisticsResponse : ApiResponse + { + } + + public class AntdFaqCreateResponse : ApiResponse + { + } + + public class AntdFaqUpdateResponse : ApiResponse + { + } + + public class AntdFaqDeleteResponse : ApiResponse + { + } +} diff --git a/Entities/Antd/AntdFaq.cs b/Entities/Antd/AntdFaq.cs new file mode 100644 index 0000000..42f7226 --- /dev/null +++ b/Entities/Antd/AntdFaq.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; + +namespace AdminHubApi.Entities.Antd +{ + public class AntdFaq + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public string Question { get; set; } = string.Empty; + + [Required] + public string Answer { get; set; } = string.Empty; + + [MaxLength(100)] + public string Category { get; set; } = string.Empty; + + public DateTime? DateCreated { get; set; } + + public bool IsFeatured { get; set; } + + public int Views { get; set; } + + public string Tags { get; set; } = string.Empty; + + public decimal Rating { get; set; } + + [MaxLength(200)] + public string Author { get; set; } = string.Empty; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/Interfaces/Antd/IAntdFaqService.cs b/Interfaces/Antd/IAntdFaqService.cs new file mode 100644 index 0000000..a19b5c3 --- /dev/null +++ b/Interfaces/Antd/IAntdFaqService.cs @@ -0,0 +1,15 @@ +using AdminHubApi.Dtos.Antd; + +namespace AdminHubApi.Interfaces.Antd +{ + public interface IAntdFaqService + { + Task GetAllAsync(AntdFaqQueryParams queryParams); + Task GetByIdAsync(string id); + Task GetStatisticsAsync(); + Task GetFeaturedAsync(int limit = 10); + Task CreateAsync(AntdFaqDto faqDto); + Task UpdateAsync(string id, AntdFaqDto faqDto); + Task DeleteAsync(string id); + } +} diff --git a/Program.cs b/Program.cs index 973c962..12b1ff4 100644 --- a/Program.cs +++ b/Program.cs @@ -196,6 +196,7 @@ await tokenBlacklistRepository.IsTokenBlacklistedAsync(tokenId)) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Repository builder.Services.AddScoped(); diff --git a/Services/Antd/AntdFaqService.cs b/Services/Antd/AntdFaqService.cs new file mode 100644 index 0000000..989c97b --- /dev/null +++ b/Services/Antd/AntdFaqService.cs @@ -0,0 +1,320 @@ +using AdminHubApi.Data; +using AdminHubApi.Dtos.ApiResponse; +using AdminHubApi.Dtos.Antd; +using AdminHubApi.Entities.Antd; +using AdminHubApi.Interfaces.Antd; +using Microsoft.EntityFrameworkCore; + +namespace AdminHubApi.Services.Antd +{ + public class AntdFaqService : IAntdFaqService + { + private readonly ApplicationDbContext _context; + private readonly ILogger _logger; + + public AntdFaqService(ApplicationDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task GetAllAsync(AntdFaqQueryParams queryParams) + { + try + { + var query = _context.AntdFaqs.AsQueryable(); + + // Apply filters + if (!string.IsNullOrEmpty(queryParams.Category)) + query = query.Where(f => f.Category.ToLower() == queryParams.Category.ToLower()); + + if (queryParams.IsFeatured.HasValue) + query = query.Where(f => f.IsFeatured == queryParams.IsFeatured.Value); + + if (queryParams.MinRating.HasValue) + query = query.Where(f => f.Rating >= queryParams.MinRating.Value); + + if (!string.IsNullOrEmpty(queryParams.SearchTerm)) + { + var searchLower = queryParams.SearchTerm.ToLower(); + query = query.Where(f => + f.Question.ToLower().Contains(searchLower) || + f.Answer.ToLower().Contains(searchLower) || + f.Tags.ToLower().Contains(searchLower) || + f.Author.ToLower().Contains(searchLower)); + } + + // Apply sorting + query = queryParams.SortBy.ToLower() switch + { + "question" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(f => f.Question) + : query.OrderBy(f => f.Question), + "category" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(f => f.Category) + : query.OrderBy(f => f.Category), + "rating" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(f => f.Rating) + : query.OrderBy(f => f.Rating), + "datecreated" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(f => f.DateCreated) + : query.OrderBy(f => f.DateCreated), + "views" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(f => f.Views) + : query.OrderBy(f => f.Views), + _ => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(f => f.Views) + : query.OrderBy(f => f.Views) + }; + + var total = await query.CountAsync(); + var faqs = await query + .Skip((queryParams.Page - 1) * queryParams.Limit) + .Take(queryParams.Limit) + .ToListAsync(); + + return new AntdFaqListResponse + { + Succeeded = true, + Data = faqs.Select(MapToDto).ToList(), + Message = "FAQs retrieved successfully", + Meta = new PaginationMeta + { + Page = queryParams.Page, + Limit = queryParams.Limit, + Total = total, + TotalPages = (int)Math.Ceiling((double)total / queryParams.Limit) + } + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd FAQs"); + throw; + } + } + + public async Task GetByIdAsync(string id) + { + try + { + if (!Guid.TryParse(id, out var guidId)) + return new AntdFaqResponse { Succeeded = false, Message = "Invalid FAQ ID format" }; + + var faq = await _context.AntdFaqs.FindAsync(guidId); + if (faq == null) + return new AntdFaqResponse { Succeeded = false, Message = "FAQ not found" }; + + // Increment view count + faq.Views++; + await _context.SaveChangesAsync(); + + return new AntdFaqResponse + { + Succeeded = true, + Data = MapToDto(faq), + Message = "FAQ retrieved successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd FAQ {FaqId}", id); + throw; + } + } + + public async Task GetStatisticsAsync() + { + try + { + var faqs = await _context.AntdFaqs.ToListAsync(); + + if (faqs.Count == 0) + { + return new AntdFaqStatisticsResponse + { + Succeeded = true, + Data = new AntdFaqStatisticsDto + { + TotalFaqs = 0, + TotalViews = 0, + AverageRating = 0, + FeaturedCount = 0, + FaqsByCategory = new Dictionary() + }, + Message = "Statistics retrieved successfully" + }; + } + + var statistics = new AntdFaqStatisticsDto + { + TotalFaqs = faqs.Count, + TotalViews = faqs.Sum(f => f.Views), + AverageRating = faqs.Average(f => f.Rating), + FeaturedCount = faqs.Count(f => f.IsFeatured), + FaqsByCategory = faqs + .GroupBy(f => f.Category) + .ToDictionary(g => g.Key, g => g.Count()) + }; + + return new AntdFaqStatisticsResponse + { + Succeeded = true, + Data = statistics, + Message = "Statistics retrieved successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd FAQ statistics"); + throw; + } + } + + public async Task GetFeaturedAsync(int limit = 10) + { + try + { + var faqs = await _context.AntdFaqs + .Where(f => f.IsFeatured) + .OrderByDescending(f => f.Views) + .Take(limit) + .ToListAsync(); + + return new AntdFaqListResponse + { + Succeeded = true, + Data = faqs.Select(MapToDto).ToList(), + Message = "Featured FAQs retrieved successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving featured Antd FAQs"); + throw; + } + } + + public async Task CreateAsync(AntdFaqDto faqDto) + { + try + { + var faq = new AntdFaq + { + Id = Guid.NewGuid(), + Question = faqDto.Question, + Answer = faqDto.Answer, + Category = faqDto.Category, + DateCreated = string.IsNullOrEmpty(faqDto.DateCreated) + ? null + : DateTime.Parse(faqDto.DateCreated).ToUniversalTime(), + IsFeatured = faqDto.IsFeatured, + Views = faqDto.Views, + Tags = faqDto.Tags, + Rating = faqDto.Rating, + Author = faqDto.Author, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + _context.AntdFaqs.Add(faq); + await _context.SaveChangesAsync(); + + return new AntdFaqCreateResponse + { + Succeeded = true, + Data = MapToDto(faq), + Message = "FAQ created successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Antd FAQ"); + throw; + } + } + + public async Task UpdateAsync(string id, AntdFaqDto faqDto) + { + try + { + if (!Guid.TryParse(id, out var guidId)) + return new AntdFaqUpdateResponse { Succeeded = false, Message = "Invalid FAQ ID format" }; + + var faq = await _context.AntdFaqs.FindAsync(guidId); + if (faq == null) + return new AntdFaqUpdateResponse { Succeeded = false, Message = "FAQ not found" }; + + faq.Question = faqDto.Question; + faq.Answer = faqDto.Answer; + faq.Category = faqDto.Category; + faq.DateCreated = string.IsNullOrEmpty(faqDto.DateCreated) + ? null + : DateTime.Parse(faqDto.DateCreated).ToUniversalTime(); + faq.IsFeatured = faqDto.IsFeatured; + faq.Tags = faqDto.Tags; + faq.Rating = faqDto.Rating; + faq.Author = faqDto.Author; + faq.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + return new AntdFaqUpdateResponse + { + Succeeded = true, + Data = MapToDto(faq), + Message = "FAQ updated successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating Antd FAQ {FaqId}", id); + throw; + } + } + + public async Task DeleteAsync(string id) + { + try + { + if (!Guid.TryParse(id, out var guidId)) + return new AntdFaqDeleteResponse { Succeeded = false, Message = "Invalid FAQ ID format" }; + + var faq = await _context.AntdFaqs.FindAsync(guidId); + if (faq == null) + return new AntdFaqDeleteResponse { Succeeded = false, Message = "FAQ not found" }; + + _context.AntdFaqs.Remove(faq); + await _context.SaveChangesAsync(); + + return new AntdFaqDeleteResponse + { + Succeeded = true, + Message = "FAQ deleted successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting Antd FAQ {FaqId}", id); + throw; + } + } + + private static AntdFaqDto MapToDto(AntdFaq faq) + { + return new AntdFaqDto + { + Id = faq.Id.ToString(), + Question = faq.Question, + Answer = faq.Answer, + Category = faq.Category, + DateCreated = faq.DateCreated?.ToString("M/d/yyyy") ?? string.Empty, + IsFeatured = faq.IsFeatured, + Views = faq.Views, + Tags = faq.Tags, + Rating = faq.Rating, + Author = faq.Author + }; + } + } +} From 2b1b64f527e71ce25baa9c92124d797fcaab5c3c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 19:15:27 +0000 Subject: [PATCH 7/9] feat: Add Antd Pricing endpoints with full CRUD operations and seed data - Add AntdPricing entity with plan, monthly, annually, savings caption, features, color, preferred - Create AntdPricingDto with query params and response types - Implement IAntdPricingService with GetAll, GetById, GetByPlan, Create, Update, Delete - Add AntdPricingsController with REST endpoints including plan-based lookup - Store features array as newline-separated string in database, convert to/from List in service - Configure entity indexes and precision in ApplicationDbContext - Add Permissions.Antd.Pricings permission constant - Register AntdPricingService in Program.cs - Add 10 diverse pricing plan seed records: free, pro, business, enterprise, starter, team, premium, developer, agency, lifetime - Plans range from $0-$99.99/month and $0-$1999.99/annually with various feature sets --- Constants/Permissions.cs | 1 + Controllers/Antd/AntdPricingsController.cs | 124 +++++++++++ Data/ApplicationDbContext.cs | 10 + Data/Seeders/AntdDataSeeder.cs | 122 ++++++++++ Dtos/Antd/AntdPricingDto.cs | 64 ++++++ Entities/Antd/AntdPricing.cs | 32 +++ Interfaces/Antd/IAntdPricingService.cs | 14 ++ Program.cs | 1 + Services/Antd/AntdPricingService.cs | 248 +++++++++++++++++++++ 9 files changed, 616 insertions(+) create mode 100644 Controllers/Antd/AntdPricingsController.cs create mode 100644 Dtos/Antd/AntdPricingDto.cs create mode 100644 Entities/Antd/AntdPricing.cs create mode 100644 Interfaces/Antd/IAntdPricingService.cs create mode 100644 Services/Antd/AntdPricingService.cs diff --git a/Constants/Permissions.cs b/Constants/Permissions.cs index e5f04cf..1740a0c 100644 --- a/Constants/Permissions.cs +++ b/Constants/Permissions.cs @@ -62,6 +62,7 @@ public static class Antd public const string TruckDeliveryRequests = "Permissions.Antd.TruckDeliveryRequests"; public const string Employees = "Permissions.Antd.Employees"; public const string Faqs = "Permissions.Antd.Faqs"; + public const string Pricings = "Permissions.Antd.Pricings"; } diff --git a/Controllers/Antd/AntdPricingsController.cs b/Controllers/Antd/AntdPricingsController.cs new file mode 100644 index 0000000..bdef037 --- /dev/null +++ b/Controllers/Antd/AntdPricingsController.cs @@ -0,0 +1,124 @@ +using AdminHubApi.Constants; +using AdminHubApi.Dtos.Antd; +using AdminHubApi.Interfaces.Antd; +using AdminHubApi.Security; +using Microsoft.AspNetCore.Mvc; + +namespace AdminHubApi.Controllers.Antd +{ + [Route("/api/v1/antd/pricings")] + [Tags("Antd - Pricings")] + [PermissionAuthorize(Permissions.Antd.Pricings)] + public class AntdPricingsController : AntdBaseController + { + private readonly IAntdPricingService _pricingService; + + public AntdPricingsController(IAntdPricingService pricingService, ILogger logger) + : base(logger) + { + _pricingService = pricingService; + } + + [HttpGet] + [ProducesResponseType(typeof(AntdPricingListResponse), 200)] + public async Task GetAllPricings([FromQuery] AntdPricingQueryParams queryParams) + { + try + { + var response = await _pricingService.GetAllAsync(queryParams); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd pricing plans"); + return ErrorResponse("Failed to retrieve pricing plans", 500); + } + } + + [HttpGet("plan/{plan}")] + [ProducesResponseType(typeof(AntdPricingResponse), 200)] + public async Task GetPricingByPlan(string plan) + { + try + { + var response = await _pricingService.GetByPlanAsync(plan); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd pricing plan {Plan}", plan); + return ErrorResponse("Failed to retrieve pricing plan", 500); + } + } + + [HttpGet("{id}")] + [ProducesResponseType(typeof(AntdPricingResponse), 200)] + public async Task GetPricingById(string id) + { + try + { + var response = await _pricingService.GetByIdAsync(id); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd pricing plan {PricingId}", id); + return ErrorResponse("Failed to retrieve pricing plan", 500); + } + } + + [HttpPost] + [ProducesResponseType(typeof(AntdPricingCreateResponse), 201)] + public async Task CreatePricing([FromBody] AntdPricingDto pricingDto) + { + try + { + if (!ModelState.IsValid) return BadRequest(ModelState); + var response = await _pricingService.CreateAsync(pricingDto); + return StatusCode(201, response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Antd pricing plan"); + return ErrorResponse("Failed to create pricing plan", 500); + } + } + + [HttpPut("{id}")] + [ProducesResponseType(typeof(AntdPricingUpdateResponse), 200)] + public async Task UpdatePricing(string id, [FromBody] AntdPricingDto pricingDto) + { + try + { + if (!ModelState.IsValid) return BadRequest(ModelState); + var response = await _pricingService.UpdateAsync(id, pricingDto); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating Antd pricing plan {PricingId}", id); + return ErrorResponse("Failed to update pricing plan", 500); + } + } + + [HttpDelete("{id}")] + [ProducesResponseType(typeof(AntdPricingDeleteResponse), 200)] + public async Task DeletePricing(string id) + { + try + { + var response = await _pricingService.DeleteAsync(id); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting Antd pricing plan {PricingId}", id); + return ErrorResponse("Failed to delete pricing plan", 500); + } + } + } +} diff --git a/Data/ApplicationDbContext.cs b/Data/ApplicationDbContext.cs index c712c36..b6ccefb 100644 --- a/Data/ApplicationDbContext.cs +++ b/Data/ApplicationDbContext.cs @@ -60,6 +60,7 @@ public ApplicationDbContext(DbContextOptions options) : ba public DbSet AntdTruckDeliveryRequests { get; set; } public DbSet AntdEmployees { get; set; } public DbSet AntdFaqs { get; set; } + public DbSet AntdPricings { get; set; } protected override void OnModelCreating(ModelBuilder builder) { @@ -424,6 +425,15 @@ protected override void OnModelCreating(ModelBuilder builder) entity.Property(e => e.Rating).HasPrecision(3, 1); }); + builder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.Plan); + entity.HasIndex(e => e.Preferred); + entity.Property(e => e.Monthly).HasPrecision(18, 2); + entity.Property(e => e.Annually).HasPrecision(18, 2); + }); + // Configure Product entity builder.Entity(entity => { diff --git a/Data/Seeders/AntdDataSeeder.cs b/Data/Seeders/AntdDataSeeder.cs index f885a61..397ecb3 100644 --- a/Data/Seeders/AntdDataSeeder.cs +++ b/Data/Seeders/AntdDataSeeder.cs @@ -1089,6 +1089,128 @@ public static async Task SeedAntdDataAsync(IServiceProvider serviceProvider) await context.SaveChangesAsync(); logger.LogInformation("Antd FAQs data seeded successfully"); } + + // Seed Antd Pricings (10 rows) + if (!await context.AntdPricings.AnyAsync()) + { + logger.LogInformation("Seeding Antd pricing plans data..."); + var pricings = new List + { + new AntdPricing + { + Id = Guid.NewGuid(), + Plan = "free", + Monthly = 0, + Annually = 0, + SavingsCaption = "Save 0%", + Features = "Basic Dashboard Templates\nLimited Widgets and Customization Options\nEmail Support", + Color = "purple", + Preferred = false + }, + new AntdPricing + { + Id = Guid.NewGuid(), + Plan = "pro", + Monthly = 9.99m, + Annually = 99.99m, + SavingsCaption = "Save 17%", + Features = "Advanced Dashboard Templates\nRich Widgets and Customization Options\nPriority Email Support\nData Export and Import\nAccess to Premium Templates Library", + Color = "maroon", + Preferred = true + }, + new AntdPricing + { + Id = Guid.NewGuid(), + Plan = "business", + Monthly = 29.99m, + Annually = 299.99m, + SavingsCaption = "Save 17%", + Features = "All Pro Features\nAdvanced Analytics and Reporting\nCustom Branding Options\nAPI Access\nPriority Phone Support\n50GB Cloud Storage\nTeam Collaboration Tools", + Color = "blue", + Preferred = false + }, + new AntdPricing + { + Id = Guid.NewGuid(), + Plan = "enterprise", + Monthly = 99.99m, + Annually = 999.99m, + SavingsCaption = "Save 17%", + Features = "All Business Features\nUnlimited Users\nDedicated Account Manager\nCustom Integration Support\nAdvanced Security Features\n500GB Cloud Storage\nSLA Guarantee\n24/7 Premium Support", + Color = "green", + Preferred = false + }, + new AntdPricing + { + Id = Guid.NewGuid(), + Plan = "starter", + Monthly = 4.99m, + Annually = 49.99m, + SavingsCaption = "Save 17%", + Features = "Basic Dashboard Templates\nStandard Widgets\nEmail Support\n5 Projects\n10GB Storage\nBasic Analytics", + Color = "cyan", + Preferred = false + }, + new AntdPricing + { + Id = Guid.NewGuid(), + Plan = "team", + Monthly = 19.99m, + Annually = 199.99m, + SavingsCaption = "Save 17%", + Features = "All Starter Features\nUnlimited Projects\n25GB Storage\nTeam Collaboration\nAdvanced Widgets\nPriority Support\nCustom Templates", + Color = "orange", + Preferred = false + }, + new AntdPricing + { + Id = Guid.NewGuid(), + Plan = "premium", + Monthly = 49.99m, + Annually = 499.99m, + SavingsCaption = "Save 17%", + Features = "All Team Features\nWhite Label Options\n100GB Storage\nAdvanced Security\nCustom Domain\nAPI Integration\nWebhooks\nPremium Support", + Color = "red", + Preferred = false + }, + new AntdPricing + { + Id = Guid.NewGuid(), + Plan = "developer", + Monthly = 14.99m, + Annually = 149.99m, + SavingsCaption = "Save 17%", + Features = "Unlimited API Calls\nWebhook Support\nAdvanced Documentation\nCode Examples\n20GB Storage\nDeveloper Support\nSandbox Environment", + Color = "indigo", + Preferred = false + }, + new AntdPricing + { + Id = Guid.NewGuid(), + Plan = "agency", + Monthly = 79.99m, + Annually = 799.99m, + SavingsCaption = "Save 17%", + Features = "All Premium Features\nClient Management Portal\nUnlimited Client Accounts\n250GB Storage\nWhite Label Everything\nReseller Pricing\nDedicated Support\nTraining Sessions", + Color = "pink", + Preferred = false + }, + new AntdPricing + { + Id = Guid.NewGuid(), + Plan = "lifetime", + Monthly = 0, + Annually = 1999.99m, + SavingsCaption = "One-time payment", + Features = "All Enterprise Features\nLifetime Access\nLifetime Updates\nPriority Feature Requests\nUnlimited Storage\nPersonal Onboarding\nLifetime Support\nExclusive Community Access", + Color = "gold", + Preferred = false + } + }; + context.AntdPricings.AddRange(pricings); + await context.SaveChangesAsync(); + logger.LogInformation("Antd pricing plans data seeded successfully"); + } } catch (Exception ex) { diff --git a/Dtos/Antd/AntdPricingDto.cs b/Dtos/Antd/AntdPricingDto.cs new file mode 100644 index 0000000..d07ebe6 --- /dev/null +++ b/Dtos/Antd/AntdPricingDto.cs @@ -0,0 +1,64 @@ +using AdminHubApi.Dtos.ApiResponse; +using System.Text.Json.Serialization; + +namespace AdminHubApi.Dtos.Antd +{ + public class AntdPricingDto + { + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("plan")] + public string Plan { get; set; } = string.Empty; + + [JsonPropertyName("monthly")] + public decimal Monthly { get; set; } + + [JsonPropertyName("annually")] + public decimal Annually { get; set; } + + [JsonPropertyName("savings_caption")] + public string SavingsCaption { get; set; } = string.Empty; + + [JsonPropertyName("features")] + public List Features { get; set; } = new(); + + [JsonPropertyName("color")] + public string Color { get; set; } = string.Empty; + + [JsonPropertyName("preferred")] + public bool Preferred { get; set; } + } + + public class AntdPricingQueryParams + { + public int Page { get; set; } = 1; + public int Limit { get; set; } = 20; + public string? Plan { get; set; } + public bool? Preferred { get; set; } + public decimal? MaxMonthly { get; set; } + public decimal? MaxAnnually { get; set; } + public string SortBy { get; set; } = "monthly"; + public string SortOrder { get; set; } = "asc"; + } + + public class AntdPricingResponse : ApiResponse + { + } + + public class AntdPricingListResponse : ApiResponse> + { + } + + public class AntdPricingCreateResponse : ApiResponse + { + } + + public class AntdPricingUpdateResponse : ApiResponse + { + } + + public class AntdPricingDeleteResponse : ApiResponse + { + } +} diff --git a/Entities/Antd/AntdPricing.cs b/Entities/Antd/AntdPricing.cs new file mode 100644 index 0000000..25ac53a --- /dev/null +++ b/Entities/Antd/AntdPricing.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace AdminHubApi.Entities.Antd +{ + public class AntdPricing + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + [MaxLength(100)] + public string Plan { get; set; } = string.Empty; + + public decimal Monthly { get; set; } + + public decimal Annually { get; set; } + + [MaxLength(100)] + public string SavingsCaption { get; set; } = string.Empty; + + // Store features as newline-separated string + public string Features { get; set; } = string.Empty; + + [MaxLength(50)] + public string Color { get; set; } = string.Empty; + + public bool Preferred { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/Interfaces/Antd/IAntdPricingService.cs b/Interfaces/Antd/IAntdPricingService.cs new file mode 100644 index 0000000..706276e --- /dev/null +++ b/Interfaces/Antd/IAntdPricingService.cs @@ -0,0 +1,14 @@ +using AdminHubApi.Dtos.Antd; + +namespace AdminHubApi.Interfaces.Antd +{ + public interface IAntdPricingService + { + Task GetAllAsync(AntdPricingQueryParams queryParams); + Task GetByIdAsync(string id); + Task GetByPlanAsync(string plan); + Task CreateAsync(AntdPricingDto pricingDto); + Task UpdateAsync(string id, AntdPricingDto pricingDto); + Task DeleteAsync(string id); + } +} diff --git a/Program.cs b/Program.cs index 12b1ff4..56c878c 100644 --- a/Program.cs +++ b/Program.cs @@ -197,6 +197,7 @@ await tokenBlacklistRepository.IsTokenBlacklistedAsync(tokenId)) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Repository builder.Services.AddScoped(); diff --git a/Services/Antd/AntdPricingService.cs b/Services/Antd/AntdPricingService.cs new file mode 100644 index 0000000..dc77083 --- /dev/null +++ b/Services/Antd/AntdPricingService.cs @@ -0,0 +1,248 @@ +using AdminHubApi.Data; +using AdminHubApi.Dtos.ApiResponse; +using AdminHubApi.Dtos.Antd; +using AdminHubApi.Entities.Antd; +using AdminHubApi.Interfaces.Antd; +using Microsoft.EntityFrameworkCore; + +namespace AdminHubApi.Services.Antd +{ + public class AntdPricingService : IAntdPricingService + { + private readonly ApplicationDbContext _context; + private readonly ILogger _logger; + + public AntdPricingService(ApplicationDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task GetAllAsync(AntdPricingQueryParams queryParams) + { + try + { + var query = _context.AntdPricings.AsQueryable(); + + // Apply filters + if (!string.IsNullOrEmpty(queryParams.Plan)) + query = query.Where(p => p.Plan.ToLower() == queryParams.Plan.ToLower()); + + if (queryParams.Preferred.HasValue) + query = query.Where(p => p.Preferred == queryParams.Preferred.Value); + + if (queryParams.MaxMonthly.HasValue) + query = query.Where(p => p.Monthly <= queryParams.MaxMonthly.Value); + + if (queryParams.MaxAnnually.HasValue) + query = query.Where(p => p.Annually <= queryParams.MaxAnnually.Value); + + // Apply sorting + query = queryParams.SortBy.ToLower() switch + { + "plan" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(p => p.Plan) + : query.OrderBy(p => p.Plan), + "annually" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(p => p.Annually) + : query.OrderBy(p => p.Annually), + "preferred" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(p => p.Preferred) + : query.OrderBy(p => p.Preferred), + _ => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(p => p.Monthly) + : query.OrderBy(p => p.Monthly) + }; + + var total = await query.CountAsync(); + var pricings = await query + .Skip((queryParams.Page - 1) * queryParams.Limit) + .Take(queryParams.Limit) + .ToListAsync(); + + return new AntdPricingListResponse + { + Succeeded = true, + Data = pricings.Select(MapToDto).ToList(), + Message = "Pricing plans retrieved successfully", + Meta = new PaginationMeta + { + Page = queryParams.Page, + Limit = queryParams.Limit, + Total = total, + TotalPages = (int)Math.Ceiling((double)total / queryParams.Limit) + } + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd pricing plans"); + throw; + } + } + + public async Task GetByIdAsync(string id) + { + try + { + if (!Guid.TryParse(id, out var guidId)) + return new AntdPricingResponse { Succeeded = false, Message = "Invalid pricing ID format" }; + + var pricing = await _context.AntdPricings.FindAsync(guidId); + if (pricing == null) + return new AntdPricingResponse { Succeeded = false, Message = "Pricing plan not found" }; + + return new AntdPricingResponse + { + Succeeded = true, + Data = MapToDto(pricing), + Message = "Pricing plan retrieved successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd pricing plan {PricingId}", id); + throw; + } + } + + public async Task GetByPlanAsync(string plan) + { + try + { + var pricing = await _context.AntdPricings + .FirstOrDefaultAsync(p => p.Plan.ToLower() == plan.ToLower()); + + if (pricing == null) + return new AntdPricingResponse { Succeeded = false, Message = "Pricing plan not found" }; + + return new AntdPricingResponse + { + Succeeded = true, + Data = MapToDto(pricing), + Message = "Pricing plan retrieved successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd pricing plan {Plan}", plan); + throw; + } + } + + public async Task CreateAsync(AntdPricingDto pricingDto) + { + try + { + var pricing = new AntdPricing + { + Id = Guid.NewGuid(), + Plan = pricingDto.Plan, + Monthly = pricingDto.Monthly, + Annually = pricingDto.Annually, + SavingsCaption = pricingDto.SavingsCaption, + Features = string.Join("\n", pricingDto.Features), + Color = pricingDto.Color, + Preferred = pricingDto.Preferred, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + _context.AntdPricings.Add(pricing); + await _context.SaveChangesAsync(); + + return new AntdPricingCreateResponse + { + Succeeded = true, + Data = MapToDto(pricing), + Message = "Pricing plan created successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Antd pricing plan"); + throw; + } + } + + public async Task UpdateAsync(string id, AntdPricingDto pricingDto) + { + try + { + if (!Guid.TryParse(id, out var guidId)) + return new AntdPricingUpdateResponse { Succeeded = false, Message = "Invalid pricing ID format" }; + + var pricing = await _context.AntdPricings.FindAsync(guidId); + if (pricing == null) + return new AntdPricingUpdateResponse { Succeeded = false, Message = "Pricing plan not found" }; + + pricing.Plan = pricingDto.Plan; + pricing.Monthly = pricingDto.Monthly; + pricing.Annually = pricingDto.Annually; + pricing.SavingsCaption = pricingDto.SavingsCaption; + pricing.Features = string.Join("\n", pricingDto.Features); + pricing.Color = pricingDto.Color; + pricing.Preferred = pricingDto.Preferred; + pricing.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + return new AntdPricingUpdateResponse + { + Succeeded = true, + Data = MapToDto(pricing), + Message = "Pricing plan updated successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating Antd pricing plan {PricingId}", id); + throw; + } + } + + public async Task DeleteAsync(string id) + { + try + { + if (!Guid.TryParse(id, out var guidId)) + return new AntdPricingDeleteResponse { Succeeded = false, Message = "Invalid pricing ID format" }; + + var pricing = await _context.AntdPricings.FindAsync(guidId); + if (pricing == null) + return new AntdPricingDeleteResponse { Succeeded = false, Message = "Pricing plan not found" }; + + _context.AntdPricings.Remove(pricing); + await _context.SaveChangesAsync(); + + return new AntdPricingDeleteResponse + { + Succeeded = true, + Message = "Pricing plan deleted successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting Antd pricing plan {PricingId}", id); + throw; + } + } + + private static AntdPricingDto MapToDto(AntdPricing pricing) + { + return new AntdPricingDto + { + Id = pricing.Id.ToString(), + Plan = pricing.Plan, + Monthly = pricing.Monthly, + Annually = pricing.Annually, + SavingsCaption = pricing.SavingsCaption, + Features = string.IsNullOrEmpty(pricing.Features) + ? new List() + : pricing.Features.Split('\n', StringSplitOptions.RemoveEmptyEntries).ToList(), + Color = pricing.Color, + Preferred = pricing.Preferred + }; + } + } +} From 6272d985326b7b2a959ffc631f8301617ff1fb31 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 19:21:30 +0000 Subject: [PATCH 8/9] feat: Add Antd License endpoints with full CRUD operations and seed data - Add AntdLicense entity with title and description - Create AntdLicenseDto with query params and response types - Implement IAntdLicenseService with GetAll, GetById, GetByTitle, Create, Update, Delete - Add AntdLicensesController with REST endpoints including title-based lookup - Configure entity indexes in ApplicationDbContext - Add Permissions.Antd.Licenses permission constant - Register AntdLicenseService in Program.cs - Add 3 license seed records: free, pro, and enterprise plans - Each license includes comprehensive terms for personal, commercial, and large-scale use --- Constants/Permissions.cs | 1 + Controllers/Antd/AntdLicensesController.cs | 124 +++++++++++ Data/ApplicationDbContext.cs | 7 + Data/Seeders/AntdDataSeeder.cs | 30 +++ Dtos/Antd/AntdLicenseDto.cs | 47 +++++ Entities/Antd/AntdLicense.cs | 20 ++ Interfaces/Antd/IAntdLicenseService.cs | 14 ++ Program.cs | 1 + Services/Antd/AntdLicenseService.cs | 227 +++++++++++++++++++++ 9 files changed, 471 insertions(+) create mode 100644 Controllers/Antd/AntdLicensesController.cs create mode 100644 Dtos/Antd/AntdLicenseDto.cs create mode 100644 Entities/Antd/AntdLicense.cs create mode 100644 Interfaces/Antd/IAntdLicenseService.cs create mode 100644 Services/Antd/AntdLicenseService.cs diff --git a/Constants/Permissions.cs b/Constants/Permissions.cs index 1740a0c..27d8023 100644 --- a/Constants/Permissions.cs +++ b/Constants/Permissions.cs @@ -63,6 +63,7 @@ public static class Antd public const string Employees = "Permissions.Antd.Employees"; public const string Faqs = "Permissions.Antd.Faqs"; public const string Pricings = "Permissions.Antd.Pricings"; + public const string Licenses = "Permissions.Antd.Licenses"; } diff --git a/Controllers/Antd/AntdLicensesController.cs b/Controllers/Antd/AntdLicensesController.cs new file mode 100644 index 0000000..bf6cc3b --- /dev/null +++ b/Controllers/Antd/AntdLicensesController.cs @@ -0,0 +1,124 @@ +using AdminHubApi.Constants; +using AdminHubApi.Dtos.Antd; +using AdminHubApi.Interfaces.Antd; +using AdminHubApi.Security; +using Microsoft.AspNetCore.Mvc; + +namespace AdminHubApi.Controllers.Antd +{ + [Route("/api/v1/antd/licenses")] + [Tags("Antd - Licenses")] + [PermissionAuthorize(Permissions.Antd.Licenses)] + public class AntdLicensesController : AntdBaseController + { + private readonly IAntdLicenseService _licenseService; + + public AntdLicensesController(IAntdLicenseService licenseService, ILogger logger) + : base(logger) + { + _licenseService = licenseService; + } + + [HttpGet] + [ProducesResponseType(typeof(AntdLicenseListResponse), 200)] + public async Task GetAllLicenses([FromQuery] AntdLicenseQueryParams queryParams) + { + try + { + var response = await _licenseService.GetAllAsync(queryParams); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd licenses"); + return ErrorResponse("Failed to retrieve licenses", 500); + } + } + + [HttpGet("title/{title}")] + [ProducesResponseType(typeof(AntdLicenseResponse), 200)] + public async Task GetLicenseByTitle(string title) + { + try + { + var response = await _licenseService.GetByTitleAsync(title); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd license {Title}", title); + return ErrorResponse("Failed to retrieve license", 500); + } + } + + [HttpGet("{id}")] + [ProducesResponseType(typeof(AntdLicenseResponse), 200)] + public async Task GetLicenseById(string id) + { + try + { + var response = await _licenseService.GetByIdAsync(id); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd license {LicenseId}", id); + return ErrorResponse("Failed to retrieve license", 500); + } + } + + [HttpPost] + [ProducesResponseType(typeof(AntdLicenseCreateResponse), 201)] + public async Task CreateLicense([FromBody] AntdLicenseDto licenseDto) + { + try + { + if (!ModelState.IsValid) return BadRequest(ModelState); + var response = await _licenseService.CreateAsync(licenseDto); + return StatusCode(201, response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Antd license"); + return ErrorResponse("Failed to create license", 500); + } + } + + [HttpPut("{id}")] + [ProducesResponseType(typeof(AntdLicenseUpdateResponse), 200)] + public async Task UpdateLicense(string id, [FromBody] AntdLicenseDto licenseDto) + { + try + { + if (!ModelState.IsValid) return BadRequest(ModelState); + var response = await _licenseService.UpdateAsync(id, licenseDto); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating Antd license {LicenseId}", id); + return ErrorResponse("Failed to update license", 500); + } + } + + [HttpDelete("{id}")] + [ProducesResponseType(typeof(AntdLicenseDeleteResponse), 200)] + public async Task DeleteLicense(string id) + { + try + { + var response = await _licenseService.DeleteAsync(id); + if (!response.Succeeded) return NotFound(response); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting Antd license {LicenseId}", id); + return ErrorResponse("Failed to delete license", 500); + } + } + } +} diff --git a/Data/ApplicationDbContext.cs b/Data/ApplicationDbContext.cs index b6ccefb..47d0a19 100644 --- a/Data/ApplicationDbContext.cs +++ b/Data/ApplicationDbContext.cs @@ -61,6 +61,7 @@ public ApplicationDbContext(DbContextOptions options) : ba public DbSet AntdEmployees { get; set; } public DbSet AntdFaqs { get; set; } public DbSet AntdPricings { get; set; } + public DbSet AntdLicenses { get; set; } protected override void OnModelCreating(ModelBuilder builder) { @@ -434,6 +435,12 @@ protected override void OnModelCreating(ModelBuilder builder) entity.Property(e => e.Annually).HasPrecision(18, 2); }); + builder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.Title); + }); + // Configure Product entity builder.Entity(entity => { diff --git a/Data/Seeders/AntdDataSeeder.cs b/Data/Seeders/AntdDataSeeder.cs index 397ecb3..938df4a 100644 --- a/Data/Seeders/AntdDataSeeder.cs +++ b/Data/Seeders/AntdDataSeeder.cs @@ -1211,6 +1211,36 @@ public static async Task SeedAntdDataAsync(IServiceProvider serviceProvider) await context.SaveChangesAsync(); logger.LogInformation("Antd pricing plans data seeded successfully"); } + + // Seed Antd Licenses (3 rows) + if (!await context.AntdLicenses.AnyAsync()) + { + logger.LogInformation("Seeding Antd licenses data..."); + var licenses = new List + { + new AntdLicense + { + Id = Guid.NewGuid(), + Title = "free", + Description = "The Free Plan grants the user a non-exclusive, non-transferable license to use the dashboard template for personal or non-commercial purposes. Users are allowed to modify the template to suit their needs. However, this license does not permit the user to sublicense, sell, or distribute the template or any derivative works. The Free Plan also includes basic support via email. The license is valid as long as the user adheres to the terms of service." + }, + new AntdLicense + { + Id = Guid.NewGuid(), + Title = "pro", + Description = "The Pro Plan includes a non-exclusive, non-transferable license that allows the user to use the dashboard template for commercial purposes. Users can modify, customize, and create derivative works based on the template. The license permits the user to sublicense the template to clients or end-users. It also includes priority email support and grants access to premium features such as data export/import and the premium templates library. The license is valid for the subscribed period (monthly or annually) and automatically renews unless canceled." + }, + new AntdLicense + { + Id = Guid.NewGuid(), + Title = "enterprise", + Description = "The Enterprise Plan provides a comprehensive, non-exclusive, non-transferable license for large-scale commercial use. This license grants the user unlimited flexibility to modify, customize, and create derivative works from the dashboard template. Users can sublicense the template, access premium support with a dedicated account manager, and utilize advanced features like API access and integration support. The license includes 24/7 priority support and ensures the highest level of data security and compliance. The license is valid for the subscribed period (monthly or annually) and automatically renews unless canceled." + } + }; + context.AntdLicenses.AddRange(licenses); + await context.SaveChangesAsync(); + logger.LogInformation("Antd licenses data seeded successfully"); + } } catch (Exception ex) { diff --git a/Dtos/Antd/AntdLicenseDto.cs b/Dtos/Antd/AntdLicenseDto.cs new file mode 100644 index 0000000..07e9b5e --- /dev/null +++ b/Dtos/Antd/AntdLicenseDto.cs @@ -0,0 +1,47 @@ +using AdminHubApi.Dtos.ApiResponse; +using System.Text.Json.Serialization; + +namespace AdminHubApi.Dtos.Antd +{ + public class AntdLicenseDto + { + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + } + + public class AntdLicenseQueryParams + { + public int Page { get; set; } = 1; + public int Limit { get; set; } = 20; + public string? Title { get; set; } + public string? SearchTerm { get; set; } + public string SortBy { get; set; } = "title"; + public string SortOrder { get; set; } = "asc"; + } + + public class AntdLicenseResponse : ApiResponse + { + } + + public class AntdLicenseListResponse : ApiResponse> + { + } + + public class AntdLicenseCreateResponse : ApiResponse + { + } + + public class AntdLicenseUpdateResponse : ApiResponse + { + } + + public class AntdLicenseDeleteResponse : ApiResponse + { + } +} diff --git a/Entities/Antd/AntdLicense.cs b/Entities/Antd/AntdLicense.cs new file mode 100644 index 0000000..4174128 --- /dev/null +++ b/Entities/Antd/AntdLicense.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace AdminHubApi.Entities.Antd +{ + public class AntdLicense + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + [MaxLength(100)] + public string Title { get; set; } = string.Empty; + + [Required] + public string Description { get; set; } = string.Empty; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/Interfaces/Antd/IAntdLicenseService.cs b/Interfaces/Antd/IAntdLicenseService.cs new file mode 100644 index 0000000..a59fb82 --- /dev/null +++ b/Interfaces/Antd/IAntdLicenseService.cs @@ -0,0 +1,14 @@ +using AdminHubApi.Dtos.Antd; + +namespace AdminHubApi.Interfaces.Antd +{ + public interface IAntdLicenseService + { + Task GetAllAsync(AntdLicenseQueryParams queryParams); + Task GetByIdAsync(string id); + Task GetByTitleAsync(string title); + Task CreateAsync(AntdLicenseDto licenseDto); + Task UpdateAsync(string id, AntdLicenseDto licenseDto); + Task DeleteAsync(string id); + } +} diff --git a/Program.cs b/Program.cs index 56c878c..a8f5e35 100644 --- a/Program.cs +++ b/Program.cs @@ -198,6 +198,7 @@ await tokenBlacklistRepository.IsTokenBlacklistedAsync(tokenId)) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Repository builder.Services.AddScoped(); diff --git a/Services/Antd/AntdLicenseService.cs b/Services/Antd/AntdLicenseService.cs new file mode 100644 index 0000000..a4c29ad --- /dev/null +++ b/Services/Antd/AntdLicenseService.cs @@ -0,0 +1,227 @@ +using AdminHubApi.Data; +using AdminHubApi.Dtos.ApiResponse; +using AdminHubApi.Dtos.Antd; +using AdminHubApi.Entities.Antd; +using AdminHubApi.Interfaces.Antd; +using Microsoft.EntityFrameworkCore; + +namespace AdminHubApi.Services.Antd +{ + public class AntdLicenseService : IAntdLicenseService + { + private readonly ApplicationDbContext _context; + private readonly ILogger _logger; + + public AntdLicenseService(ApplicationDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task GetAllAsync(AntdLicenseQueryParams queryParams) + { + try + { + var query = _context.AntdLicenses.AsQueryable(); + + // Apply filters + if (!string.IsNullOrEmpty(queryParams.Title)) + query = query.Where(l => l.Title.ToLower() == queryParams.Title.ToLower()); + + if (!string.IsNullOrEmpty(queryParams.SearchTerm)) + { + var searchLower = queryParams.SearchTerm.ToLower(); + query = query.Where(l => + l.Title.ToLower().Contains(searchLower) || + l.Description.ToLower().Contains(searchLower)); + } + + // Apply sorting + query = queryParams.SortBy.ToLower() switch + { + "title" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(l => l.Title) + : query.OrderBy(l => l.Title), + "createdat" => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(l => l.CreatedAt) + : query.OrderBy(l => l.CreatedAt), + _ => queryParams.SortOrder.ToLower() == "desc" + ? query.OrderByDescending(l => l.Title) + : query.OrderBy(l => l.Title) + }; + + var total = await query.CountAsync(); + var licenses = await query + .Skip((queryParams.Page - 1) * queryParams.Limit) + .Take(queryParams.Limit) + .ToListAsync(); + + return new AntdLicenseListResponse + { + Succeeded = true, + Data = licenses.Select(MapToDto).ToList(), + Message = "Licenses retrieved successfully", + Meta = new PaginationMeta + { + Page = queryParams.Page, + Limit = queryParams.Limit, + Total = total, + TotalPages = (int)Math.Ceiling((double)total / queryParams.Limit) + } + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd licenses"); + throw; + } + } + + public async Task GetByIdAsync(string id) + { + try + { + if (!Guid.TryParse(id, out var guidId)) + return new AntdLicenseResponse { Succeeded = false, Message = "Invalid license ID format" }; + + var license = await _context.AntdLicenses.FindAsync(guidId); + if (license == null) + return new AntdLicenseResponse { Succeeded = false, Message = "License not found" }; + + return new AntdLicenseResponse + { + Succeeded = true, + Data = MapToDto(license), + Message = "License retrieved successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd license {LicenseId}", id); + throw; + } + } + + public async Task GetByTitleAsync(string title) + { + try + { + var license = await _context.AntdLicenses + .FirstOrDefaultAsync(l => l.Title.ToLower() == title.ToLower()); + + if (license == null) + return new AntdLicenseResponse { Succeeded = false, Message = "License not found" }; + + return new AntdLicenseResponse + { + Succeeded = true, + Data = MapToDto(license), + Message = "License retrieved successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving Antd license {Title}", title); + throw; + } + } + + public async Task CreateAsync(AntdLicenseDto licenseDto) + { + try + { + var license = new AntdLicense + { + Id = Guid.NewGuid(), + Title = licenseDto.Title, + Description = licenseDto.Description, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + _context.AntdLicenses.Add(license); + await _context.SaveChangesAsync(); + + return new AntdLicenseCreateResponse + { + Succeeded = true, + Data = MapToDto(license), + Message = "License created successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Antd license"); + throw; + } + } + + public async Task UpdateAsync(string id, AntdLicenseDto licenseDto) + { + try + { + if (!Guid.TryParse(id, out var guidId)) + return new AntdLicenseUpdateResponse { Succeeded = false, Message = "Invalid license ID format" }; + + var license = await _context.AntdLicenses.FindAsync(guidId); + if (license == null) + return new AntdLicenseUpdateResponse { Succeeded = false, Message = "License not found" }; + + license.Title = licenseDto.Title; + license.Description = licenseDto.Description; + license.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + return new AntdLicenseUpdateResponse + { + Succeeded = true, + Data = MapToDto(license), + Message = "License updated successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating Antd license {LicenseId}", id); + throw; + } + } + + public async Task DeleteAsync(string id) + { + try + { + if (!Guid.TryParse(id, out var guidId)) + return new AntdLicenseDeleteResponse { Succeeded = false, Message = "Invalid license ID format" }; + + var license = await _context.AntdLicenses.FindAsync(guidId); + if (license == null) + return new AntdLicenseDeleteResponse { Succeeded = false, Message = "License not found" }; + + _context.AntdLicenses.Remove(license); + await _context.SaveChangesAsync(); + + return new AntdLicenseDeleteResponse + { + Succeeded = true, + Message = "License deleted successfully" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting Antd license {LicenseId}", id); + throw; + } + } + + private static AntdLicenseDto MapToDto(AntdLicense license) + { + return new AntdLicenseDto + { + Id = license.Id.ToString(), + Title = license.Title, + Description = license.Description + }; + } + } +} From 6ac247747f9d69169752f70c423f242add6ca312 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 19:51:23 +0000 Subject: [PATCH 9/9] refactor: Add Tags attributes to all Antd controllers with readable names - Add [Tags] attributes to 16 controllers that were missing them - Use consistent naming pattern: "Antd - [Readable Name]" with proper spacing - Updated controllers: * Social Media Activities/Stats * Scheduled Posts * Recommended Courses * Live Auctions * Delivery Analytics * Exams, Courses * Community Groups * Bidding Top Sellers/Transactions * Auction Creators * Study Statistics * Truck Deliveries/Delivery Requests/Trucks - Improves API documentation organization in Swagger/Scalar UI --- Controllers/Antd/AntdAuctionCreatorsController.cs | 1 + Controllers/Antd/AntdBiddingTopSellersController.cs | 1 + Controllers/Antd/AntdBiddingTransactionsController.cs | 1 + Controllers/Antd/AntdCommunityGroupsController.cs | 1 + Controllers/Antd/AntdCoursesController.cs | 1 + Controllers/Antd/AntdDeliveryAnalyticsController.cs | 1 + Controllers/Antd/AntdExamsController.cs | 1 + Controllers/Antd/AntdLiveAuctionsController.cs | 1 + Controllers/Antd/AntdRecommendedCoursesController.cs | 1 + Controllers/Antd/AntdScheduledPostsController.cs | 1 + Controllers/Antd/AntdSocialMediaActivitiesController.cs | 1 + Controllers/Antd/AntdSocialMediaStatsController.cs | 1 + Controllers/Antd/AntdStudyStatisticsController.cs | 1 + Controllers/Antd/AntdTruckDeliveriesController.cs | 1 + Controllers/Antd/AntdTruckDeliveryRequestsController.cs | 1 + Controllers/Antd/AntdTrucksController.cs | 1 + 16 files changed, 16 insertions(+) diff --git a/Controllers/Antd/AntdAuctionCreatorsController.cs b/Controllers/Antd/AntdAuctionCreatorsController.cs index afd88c5..5508030 100644 --- a/Controllers/Antd/AntdAuctionCreatorsController.cs +++ b/Controllers/Antd/AntdAuctionCreatorsController.cs @@ -7,6 +7,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/auction-creators")] + [Tags("Antd - Auction Creators")] [ApiController] [PermissionAuthorize(Permissions.Antd.AuctionCreators)] public class AntdAuctionCreatorsController : ControllerBase diff --git a/Controllers/Antd/AntdBiddingTopSellersController.cs b/Controllers/Antd/AntdBiddingTopSellersController.cs index f936558..644a3be 100644 --- a/Controllers/Antd/AntdBiddingTopSellersController.cs +++ b/Controllers/Antd/AntdBiddingTopSellersController.cs @@ -7,6 +7,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/bidding-top-sellers")] + [Tags("Antd - Bidding Top Sellers")] [ApiController] [PermissionAuthorize(Permissions.Antd.BiddingTopSellers)] public class AntdBiddingTopSellersController : ControllerBase diff --git a/Controllers/Antd/AntdBiddingTransactionsController.cs b/Controllers/Antd/AntdBiddingTransactionsController.cs index 2bd27cb..60224e8 100644 --- a/Controllers/Antd/AntdBiddingTransactionsController.cs +++ b/Controllers/Antd/AntdBiddingTransactionsController.cs @@ -7,6 +7,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/bidding-transactions")] + [Tags("Antd - Bidding Transactions")] [ApiController] [PermissionAuthorize(Permissions.Antd.BiddingTransactions)] public class AntdBiddingTransactionsController : ControllerBase diff --git a/Controllers/Antd/AntdCommunityGroupsController.cs b/Controllers/Antd/AntdCommunityGroupsController.cs index 7407e31..68af911 100644 --- a/Controllers/Antd/AntdCommunityGroupsController.cs +++ b/Controllers/Antd/AntdCommunityGroupsController.cs @@ -8,6 +8,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/community-groups")] + [Tags("Antd - Community Groups")] [Authorize] public class AntdCommunityGroupsController : AntdBaseController { diff --git a/Controllers/Antd/AntdCoursesController.cs b/Controllers/Antd/AntdCoursesController.cs index b4b8148..d1024f6 100644 --- a/Controllers/Antd/AntdCoursesController.cs +++ b/Controllers/Antd/AntdCoursesController.cs @@ -7,6 +7,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/courses")] + [Tags("Antd - Courses")] [ApiController] [PermissionAuthorize(Permissions.Antd.Courses)] public class AntdCoursesController : ControllerBase diff --git a/Controllers/Antd/AntdDeliveryAnalyticsController.cs b/Controllers/Antd/AntdDeliveryAnalyticsController.cs index 7c7f3f4..1d0f534 100644 --- a/Controllers/Antd/AntdDeliveryAnalyticsController.cs +++ b/Controllers/Antd/AntdDeliveryAnalyticsController.cs @@ -7,6 +7,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/delivery-analytics")] + [Tags("Antd - Delivery Analytics")] [ApiController] [PermissionAuthorize(Permissions.Antd.DeliveryAnalytics)] public class AntdDeliveryAnalyticsController : ControllerBase diff --git a/Controllers/Antd/AntdExamsController.cs b/Controllers/Antd/AntdExamsController.cs index f4620ac..291b85b 100644 --- a/Controllers/Antd/AntdExamsController.cs +++ b/Controllers/Antd/AntdExamsController.cs @@ -8,6 +8,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/exams")] + [Tags("Antd - Exams")] [Authorize] public class AntdExamsController : AntdBaseController { diff --git a/Controllers/Antd/AntdLiveAuctionsController.cs b/Controllers/Antd/AntdLiveAuctionsController.cs index fd2d894..753d626 100644 --- a/Controllers/Antd/AntdLiveAuctionsController.cs +++ b/Controllers/Antd/AntdLiveAuctionsController.cs @@ -7,6 +7,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/live-auctions")] + [Tags("Antd - Live Auctions")] [ApiController] [PermissionAuthorize(Permissions.Antd.LiveAuctions)] public class AntdLiveAuctionsController : ControllerBase diff --git a/Controllers/Antd/AntdRecommendedCoursesController.cs b/Controllers/Antd/AntdRecommendedCoursesController.cs index eee50b7..d30aa64 100644 --- a/Controllers/Antd/AntdRecommendedCoursesController.cs +++ b/Controllers/Antd/AntdRecommendedCoursesController.cs @@ -8,6 +8,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/recommended-courses")] + [Tags("Antd - Recommended Courses")] [Authorize] public class AntdRecommendedCoursesController : AntdBaseController { diff --git a/Controllers/Antd/AntdScheduledPostsController.cs b/Controllers/Antd/AntdScheduledPostsController.cs index 0f752ca..affa220 100644 --- a/Controllers/Antd/AntdScheduledPostsController.cs +++ b/Controllers/Antd/AntdScheduledPostsController.cs @@ -7,6 +7,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/scheduled-posts")] + [Tags("Antd - Scheduled Posts")] [ApiController] [PermissionAuthorize(Permissions.Antd.ScheduledPosts)] public class AntdScheduledPostsController : ControllerBase diff --git a/Controllers/Antd/AntdSocialMediaActivitiesController.cs b/Controllers/Antd/AntdSocialMediaActivitiesController.cs index a5a927f..dc5fb86 100644 --- a/Controllers/Antd/AntdSocialMediaActivitiesController.cs +++ b/Controllers/Antd/AntdSocialMediaActivitiesController.cs @@ -7,6 +7,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/social-media-activities")] + [Tags("Antd - Social Media Activities")] [ApiController] [PermissionAuthorize(Permissions.Antd.SocialMediaActivities)] public class AntdSocialMediaActivitiesController : ControllerBase diff --git a/Controllers/Antd/AntdSocialMediaStatsController.cs b/Controllers/Antd/AntdSocialMediaStatsController.cs index 6b6f59b..794e476 100644 --- a/Controllers/Antd/AntdSocialMediaStatsController.cs +++ b/Controllers/Antd/AntdSocialMediaStatsController.cs @@ -7,6 +7,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/social-media-stats")] + [Tags("Antd - Social Media Stats")] [ApiController] [PermissionAuthorize(Permissions.Antd.SocialMediaStats)] public class AntdSocialMediaStatsController : ControllerBase diff --git a/Controllers/Antd/AntdStudyStatisticsController.cs b/Controllers/Antd/AntdStudyStatisticsController.cs index 45827c9..60be6e9 100644 --- a/Controllers/Antd/AntdStudyStatisticsController.cs +++ b/Controllers/Antd/AntdStudyStatisticsController.cs @@ -8,6 +8,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/study-statistics")] + [Tags("Antd - Study Statistics")] [Authorize] public class AntdStudyStatisticsController : AntdBaseController { diff --git a/Controllers/Antd/AntdTruckDeliveriesController.cs b/Controllers/Antd/AntdTruckDeliveriesController.cs index 8d97f33..84930d7 100644 --- a/Controllers/Antd/AntdTruckDeliveriesController.cs +++ b/Controllers/Antd/AntdTruckDeliveriesController.cs @@ -7,6 +7,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/truck-deliveries")] + [Tags("Antd - Truck Deliveries")] [ApiController] [PermissionAuthorize(Permissions.Antd.TruckDeliveries)] public class AntdTruckDeliveriesController : ControllerBase diff --git a/Controllers/Antd/AntdTruckDeliveryRequestsController.cs b/Controllers/Antd/AntdTruckDeliveryRequestsController.cs index c2e1dc9..c6f4199 100644 --- a/Controllers/Antd/AntdTruckDeliveryRequestsController.cs +++ b/Controllers/Antd/AntdTruckDeliveryRequestsController.cs @@ -7,6 +7,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/truck-delivery-requests")] + [Tags("Antd - Truck Delivery Requests")] [ApiController] [PermissionAuthorize(Permissions.Antd.TruckDeliveryRequests)] public class AntdTruckDeliveryRequestsController : ControllerBase diff --git a/Controllers/Antd/AntdTrucksController.cs b/Controllers/Antd/AntdTrucksController.cs index 1708aa0..506b50f 100644 --- a/Controllers/Antd/AntdTrucksController.cs +++ b/Controllers/Antd/AntdTrucksController.cs @@ -7,6 +7,7 @@ namespace AdminHubApi.Controllers.Antd { [Route("api/v1/antd/trucks")] + [Tags("Antd - Trucks")] [ApiController] [PermissionAuthorize(Permissions.Antd.Trucks)] public class AntdTrucksController : ControllerBase