diff --git a/src/Api/Data/Interfaces/IIssueRepository.cs b/src/Api/Data/Interfaces/IIssueRepository.cs index 323a10a..dc5650d 100644 --- a/src/Api/Data/Interfaces/IIssueRepository.cs +++ b/src/Api/Data/Interfaces/IIssueRepository.cs @@ -42,8 +42,10 @@ public interface IIssueRepository /// The number of items per page. /// Optional search term to filter by title or description. /// Optional author name to filter by. + /// Optional status name to filter by. + /// Optional category name to filter by. /// Cancellation token. - Task Items, long Total)>> GetAllAsync(int page, int pageSize, string? searchTerm = null, string? authorName = null, CancellationToken cancellationToken = default); + Task Items, long Total)>> GetAllAsync(int page, int pageSize, string? searchTerm = null, string? authorName = null, string? statusName = null, string? categoryName = null, CancellationToken cancellationToken = default); /// /// Updates an existing issue in the database. diff --git a/src/Api/Data/IssueRepository.cs b/src/Api/Data/IssueRepository.cs index 916ed94..da2a5cf 100644 --- a/src/Api/Data/IssueRepository.cs +++ b/src/Api/Data/IssueRepository.cs @@ -72,6 +72,8 @@ public async Task>> GetAllAsync(CancellationToken int pageSize, string? searchTerm = null, string? authorName = null, + string? statusName = null, + string? categoryName = null, CancellationToken cancellationToken = default) { var filterBuilder = Builders.Filter; @@ -94,6 +96,16 @@ public async Task>> GetAllAsync(CancellationToken filters.Add(filterBuilder.Regex(x => x.Author.Name, new BsonRegularExpression(authorName, "i"))); } + if (!string.IsNullOrWhiteSpace(statusName)) + { + filters.Add(filterBuilder.Regex(x => x.Status.StatusName, new BsonRegularExpression(statusName, "i"))); + } + + if (!string.IsNullOrWhiteSpace(categoryName)) + { + filters.Add(filterBuilder.Regex(x => x.Category.CategoryName, new BsonRegularExpression(categoryName, "i"))); + } + var filter = filterBuilder.And(filters); var total = await _collection.CountDocumentsAsync(filter, cancellationToken: cancellationToken); var entities = await _collection diff --git a/src/Api/Handlers/Issues/IssueEndpoints.cs b/src/Api/Handlers/Issues/IssueEndpoints.cs index 239b0f0..b41ee0e 100644 --- a/src/Api/Handlers/Issues/IssueEndpoints.cs +++ b/src/Api/Handlers/Issues/IssueEndpoints.cs @@ -21,14 +21,16 @@ public static IEndpointRouteBuilder MapIssueEndpoints(this IEndpointRouteBuilder var group = app.MapGroup("/api/v1/issues").WithTags("Issues"); // List Issues (paginated) - group.MapGet("", async (int? page, int? pageSize, string? searchTerm, string? authorName, ListIssuesHandler handler) => + group.MapGet("", async (int? page, int? pageSize, string? searchTerm, string? authorName, string? statusName, string? categoryName, ListIssuesHandler handler) => { var query = new ListIssuesQuery { Page = page ?? 1, PageSize = pageSize ?? 20, SearchTerm = searchTerm, - AuthorName = authorName + AuthorName = authorName, + StatusName = statusName, + CategoryName = categoryName }; var result = await handler.Handle(query); return Results.Ok(result); diff --git a/src/Api/Handlers/Issues/ListIssuesHandler.cs b/src/Api/Handlers/Issues/ListIssuesHandler.cs index 4c40615..92e1566 100644 --- a/src/Api/Handlers/Issues/ListIssuesHandler.cs +++ b/src/Api/Handlers/Issues/ListIssuesHandler.cs @@ -35,7 +35,7 @@ public async Task> Handle(ListIssuesQuery query, Can if (!validationResult.IsValid) throw new ValidationException(validationResult.Errors); - var result = await _repository.GetAllAsync(query.Page, query.PageSize, query.SearchTerm, query.AuthorName, cancellationToken); + var result = await _repository.GetAllAsync(query.Page, query.PageSize, query.SearchTerm, query.AuthorName, query.StatusName, query.CategoryName, cancellationToken); var (items, total) = result.Value; return new PaginatedResponse(items, total, query.Page, query.PageSize); diff --git a/src/Shared/Contracts/ListIssuesQuery.cs b/src/Shared/Contracts/ListIssuesQuery.cs index 6b8d842..c0cb71f 100644 --- a/src/Shared/Contracts/ListIssuesQuery.cs +++ b/src/Shared/Contracts/ListIssuesQuery.cs @@ -32,4 +32,14 @@ public record ListIssuesQuery /// Gets or sets the author name for filtering by author. /// public string? AuthorName { get; init; } + + /// + /// Gets or sets the status name for filtering by status. + /// + public string? StatusName { get; init; } + + /// + /// Gets or sets the category name for filtering by category. + /// + public string? CategoryName { get; init; } } diff --git a/src/Web/Components/Features/Issues/IssueApiClient.cs b/src/Web/Components/Features/Issues/IssueApiClient.cs index 0a9e512..5417574 100644 --- a/src/Web/Components/Features/Issues/IssueApiClient.cs +++ b/src/Web/Components/Features/Issues/IssueApiClient.cs @@ -17,8 +17,10 @@ public interface IIssueApiClient /// The number of items per page. /// Optional search term to filter by title or description. /// Optional author name to filter by. + /// Optional status name to filter by. + /// Optional category name to filter by. /// Cancellation token. - Task> GetAllAsync(int page = 1, int pageSize = 20, string? searchTerm = null, string? authorName = null, CancellationToken cancellationToken = default); + Task> GetAllAsync(int page = 1, int pageSize = 20, string? searchTerm = null, string? authorName = null, string? statusName = null, string? categoryName = null, CancellationToken cancellationToken = default); /// Gets an issue by its identifier. Task GetByIdAsync(string id, CancellationToken cancellationToken = default); @@ -45,7 +47,7 @@ public class IssueApiClient : IIssueApiClient public IssueApiClient(HttpClient httpClient) => _httpClient = httpClient; /// - public async Task> GetAllAsync(int page = 1, int pageSize = 20, string? searchTerm = null, string? authorName = null, CancellationToken cancellationToken = default) + public async Task> GetAllAsync(int page = 1, int pageSize = 20, string? searchTerm = null, string? authorName = null, string? statusName = null, string? categoryName = null, CancellationToken cancellationToken = default) { try { @@ -58,6 +60,14 @@ public async Task> GetAllAsync(int page = 1, int pag { url += $"&authorName={Uri.EscapeDataString(authorName)}"; } + if (!string.IsNullOrWhiteSpace(statusName)) + { + url += $"&statusName={Uri.EscapeDataString(statusName)}"; + } + if (!string.IsNullOrWhiteSpace(categoryName)) + { + url += $"&categoryName={Uri.EscapeDataString(categoryName)}"; + } var result = await _httpClient.GetFromJsonAsync>(url, cancellationToken).ConfigureAwait(false); return result ?? PaginatedResponse.Empty; diff --git a/src/Web/Components/Features/Issues/IssuesPage.razor b/src/Web/Components/Features/Issues/IssuesPage.razor index 068efbc..a7f470b 100644 --- a/src/Web/Components/Features/Issues/IssuesPage.razor +++ b/src/Web/Components/Features/Issues/IssuesPage.razor @@ -153,7 +153,7 @@ _currentPage = page; try { - var response = await IssueClient.GetAllAsync(page, 20); + var response = await IssueClient.GetAllAsync(page, 20, _searchTerm, null, _statusFilter, _categoryFilter); _issues = response.Items; _totalPages = response.TotalPages; } diff --git a/tests/Api.Tests.Unit/Endpoints/IssueEndpointsTests.cs b/tests/Api.Tests.Unit/Endpoints/IssueEndpointsTests.cs index 9579141..c0beedf 100644 --- a/tests/Api.Tests.Unit/Endpoints/IssueEndpointsTests.cs +++ b/tests/Api.Tests.Unit/Endpoints/IssueEndpointsTests.cs @@ -39,7 +39,7 @@ public async Task ListIssues_ReturnsOk() // Arrange IReadOnlyList items = []; _factory.IssueRepository - .GetAllAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .GetAllAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Result<(IReadOnlyList Items, long Total)>.Ok((items, 0L))); // Act diff --git a/tests/Api.Tests.Unit/Handlers/Issues/ListIssuesHandlerTests.cs b/tests/Api.Tests.Unit/Handlers/Issues/ListIssuesHandlerTests.cs index bbc29d0..3bb55d3 100644 --- a/tests/Api.Tests.Unit/Handlers/Issues/ListIssuesHandlerTests.cs +++ b/tests/Api.Tests.Unit/Handlers/Issues/ListIssuesHandlerTests.cs @@ -33,7 +33,7 @@ public async Task Handle_DefaultPagination_ReturnsFirstPageWithCorrectMetadata() var query = new ListIssuesQuery { Page = 1, PageSize = 20 }; var issues = GenerateIssueDtos(20); - _repository.GetAllAsync(1, 20, null, null, Arg.Any()) + _repository.GetAllAsync(1, 20, null, null, null, null, Arg.Any()) .Returns(((IReadOnlyList)issues, 42L)); // Act @@ -54,7 +54,7 @@ public async Task Handle_SecondPage_ReturnsCorrectItems() var query = new ListIssuesQuery { Page = 2, PageSize = 10 }; var issues = GenerateIssueDtos(10); - _repository.GetAllAsync(2, 10, null, null, Arg.Any()) + _repository.GetAllAsync(2, 10, null, null, null, null, Arg.Any()) .Returns(((IReadOnlyList)issues, 42L)); // Act @@ -75,7 +75,7 @@ public async Task Handle_LastPagePartialItems_ReturnsCorrectCount() var query = new ListIssuesQuery { Page = 3, PageSize = 20 }; var issues = GenerateIssueDtos(2); // Last page has only 2 items - _repository.GetAllAsync(3, 20, null, null, Arg.Any()) + _repository.GetAllAsync(3, 20, null, null, null, null, Arg.Any()) .Returns(((IReadOnlyList)issues, 42L)); // Act @@ -95,7 +95,7 @@ public async Task Handle_EmptyResult_ReturnsEmptyList() // Arrange var query = new ListIssuesQuery { Page = 1, PageSize = 20 }; - _repository.GetAllAsync(1, 20, null, null, Arg.Any()) + _repository.GetAllAsync(1, 20, null, null, null, null, Arg.Any()) .Returns(((IReadOnlyList)new List(), 0L)); // Act @@ -115,7 +115,7 @@ public async Task Handle_PageExceedsTotalPages_ReturnsEmptyList() // Arrange var query = new ListIssuesQuery { Page = 10, PageSize = 20 }; - _repository.GetAllAsync(10, 20, null, null, Arg.Any()) + _repository.GetAllAsync(10, 20, null, null, null, null, Arg.Any()) .Returns(((IReadOnlyList)new List(), 42L)); // Act @@ -176,14 +176,14 @@ public async Task Handle_ExcludesArchivedIssues_ByDefault() var query = new ListIssuesQuery { Page = 1, PageSize = 20 }; var issues = GenerateIssueDtos(10); - _repository.GetAllAsync(1, 20, null, null, Arg.Any()) + _repository.GetAllAsync(1, 20, null, null, null, null, Arg.Any()) .Returns(((IReadOnlyList)issues, 10L)); // Act var result = await _handler.Handle(query, CancellationToken.None); // Assert - await _repository.Received(1).GetAllAsync(1, 20, null, null, Arg.Any()); + await _repository.Received(1).GetAllAsync(1, 20, null, null, null, null, Arg.Any()); result.Items.Should().HaveCount(10); } @@ -201,7 +201,7 @@ public async Task Handle_OrdersByCreatedAtDescending_NewestFirst() new(ObjectId.GenerateNewId(), "Issue 1", "Desc", DateTime.UtcNow.AddDays(-3), null, UserDto.Empty, CategoryDto.Empty, StatusDto.Empty, false, UserDto.Empty, false, false), }; - _repository.GetAllAsync(1, 3, null, null, Arg.Any()) + _repository.GetAllAsync(1, 3, null, null, null, null, Arg.Any()) .Returns(((IReadOnlyList)orderedIssues, 3L)); // Act