From 72979c95374724d42327c0e4c9c86c2bcc3e8662 Mon Sep 17 00:00:00 2001 From: Sebastian Frick Date: Fri, 20 Mar 2026 16:33:20 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Paginierung=20f=C3=BCr=20alle=20GET-Lis?= =?UTF-8?q?t-Endpoints=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Einheitliches Paginierungs-Pattern mit page, size und Multi-Field sort für alle 14 List-Endpoints. Response-Format ändert sich von [...] zu { content: [...], page: { number, size, totalElements, totalPages } }. Backend: - Shared Kernel: Page, PageRequest, SortField, SortDirection - PaginationHelper (SQL ORDER BY mit Whitelist), PageResponse DTO - Paginated Methoden in allen 14 Domain-Repos + JDBC-Implementierungen - Safety-Limit (500) für findAllBelowMinimumLevel/ExpiryRelevantBatches - Alle List-Use-Cases akzeptieren PageRequest, liefern Page - Alle Controller mit page/size/sort Query-Params + PageResponse Frontend: - PagedResponse Type auf nested page-Format aktualisiert - Alle 14 API-Client-Resourcen liefern PagedResponse mit PaginationParams - Alle Hooks mit Pagination-State (currentPage, totalPages, pageSize) - Alle List-Screens mit Seiten-Navigation (Pfeiltasten) und Footer Loadtest: - Podman-Support im justfile (DOCKER_HOST auto-detect) - Verschärfte Performance-Schwellwerte basierend auf Ist-Werten --- .../inventory/ListInventoryCounts.java | 33 +--- .../inventory/ListStockMovements.java | 54 ++---- .../application/inventory/ListStocks.java | 16 +- .../inventory/ListStorageLocations.java | 42 ++--- .../masterdata/article/ListArticles.java | 35 +--- .../masterdata/customer/ListCustomers.java | 34 +--- .../ListProductCategories.java | 16 +- .../masterdata/supplier/ListSuppliers.java | 25 +-- .../application/production/ListBatches.java | 10 +- .../production/ListProductionOrders.java | 9 +- .../application/production/ListRecipes.java | 10 +- .../application/shared/ListCountries.java | 8 +- .../application/usermanagement/ListUsers.java | 28 +-- .../inventory/InventoryCountRepository.java | 4 + .../inventory/StockMovementRepository.java | 7 + .../domain/inventory/StockRepository.java | 10 + .../inventory/StorageLocationRepository.java | 4 + .../masterdata/article/ArticleRepository.java | 4 + .../customer/CustomerRepository.java | 4 + .../ProductCategoryRepository.java | 4 + .../supplier/SupplierRepository.java | 4 + .../domain/production/BatchRepository.java | 4 + .../production/ProductionOrderRepository.java | 4 + .../domain/production/RecipeRepository.java | 4 + .../domain/usermanagement/RoleRepository.java | 4 + .../domain/usermanagement/UserRepository.java | 4 + .../JdbcInventoryCountRepository.java | 37 ++++ .../JdbcStockMovementRepository.java | 65 +++++++ .../repository/JdbcStockRepository.java | 108 ++++++++++- .../JdbcStorageLocationRepository.java | 42 +++++ .../controller/InventoryCountController.java | 21 ++- .../web/controller/StockController.java | 22 ++- .../controller/StockMovementController.java | 20 +- .../controller/StorageLocationController.java | 22 ++- .../persistence/JdbcArticleRepository.java | 37 +++- .../persistence/JdbcCustomerRepository.java | 26 +++ .../JdbcProductCategoryRepository.java | 26 ++- .../persistence/JdbcSupplierRepository.java | 34 ++++ .../web/controller/ArticleController.java | 25 ++- .../web/controller/CustomerController.java | 27 ++- .../controller/ProductCategoryController.java | 17 +- .../web/controller/SupplierController.java | 21 ++- .../persistence/JdbcBatchRepository.java | 35 +++- .../JdbcProductionOrderRepository.java | 31 +++- .../persistence/JdbcRecipeRepository.java | 32 +++- .../web/controller/BatchController.java | 36 +++- .../controller/ProductionOrderController.java | 35 +++- .../web/controller/RecipeController.java | 29 ++- .../shared/InMemoryCountryRepository.java | 11 ++ .../shared/persistence/PaginationHelper.java | 23 +++ .../shared/web/CountryController.java | 19 +- .../shared/web/dto/PageResponse.java | 26 +++ .../stub/StubArticleRepository.java | 7 + .../stub/StubBatchRepository.java | 7 + .../stub/StubCustomerRepository.java | 7 + .../stub/StubInventoryCountRepository.java | 7 + .../stub/StubProductCategoryRepository.java | 7 + .../stub/StubProductionOrderRepository.java | 7 + .../stub/StubRecipeRepository.java | 7 + .../stub/StubRoleRepository.java | 7 + .../stub/StubStockMovementRepository.java | 10 + .../stub/StubStockRepository.java | 22 +++ .../stub/StubStorageLocationRepository.java | 7 + .../stub/StubSupplierRepository.java | 7 + .../stub/StubUserRepository.java | 7 + .../persistence/JdbcRoleRepository.java | 26 ++- .../persistence/JdbcUserRepository.java | 36 +++- .../web/controller/RoleController.java | 24 ++- .../web/controller/UserController.java | 17 +- .../shared/common/CountryRepository.java | 1 + .../java/de/effigenix/shared/common/Page.java | 25 +++ .../effigenix/shared/common/PageRequest.java | 30 +++ .../shared/common/SortDirection.java | 5 + .../de/effigenix/shared/common/SortField.java | 31 ++++ .../inventory/CheckStockExpiryTest.java | 6 + .../inventory/ListInventoryCountsTest.java | 91 +++------ .../inventory/ListStockMovementsTest.java | 173 ++++++++---------- .../inventory/ListStocksBelowMinimumTest.java | 6 + .../application/inventory/ListStocksTest.java | 66 ++++--- .../inventory/ListStorageLocationsTest.java | 128 ++++++------- .../masterdata/ArticleUseCaseTest.java | 63 +++---- .../masterdata/CustomerUseCaseTest.java | 79 +++----- .../ProductCategoryUseCaseTest.java | 23 ++- .../masterdata/SupplierUseCaseTest.java | 37 ++-- .../production/ListBatchesTest.java | 25 ++- .../production/ListProductionOrdersTest.java | 19 +- .../production/ListRecipesTest.java | 12 +- .../application/shared/ListCountriesTest.java | 61 +++--- .../usermanagement/ListUsersTest.java | 34 ++-- .../BatchTraceabilityServiceFuzzTest.java | 6 +- ...ventoryCountControllerIntegrationTest.java | 17 +- .../web/StockControllerIntegrationTest.java | 18 +- ...tockMovementControllerIntegrationTest.java | 42 ++--- ...rageLocationControllerIntegrationTest.java | 46 ++--- .../web/ArticleControllerIntegrationTest.java | 14 +- .../CustomerControllerIntegrationTest.java | 36 +--- ...ductCategoryControllerIntegrationTest.java | 4 +- .../SupplierControllerIntegrationTest.java | 4 +- .../web/ListBatchesIntegrationTest.java | 22 +-- .../web/ListRecipesIntegrationTest.java | 18 +- ...ductionOrderControllerIntegrationTest.java | 10 +- .../persistence/PaginationHelperTest.java | 63 +++++++ .../web/CountryControllerIntegrationTest.java | 9 +- .../web/RoleControllerIntegrationTest.java | 2 +- .../web/UserControllerIntegrationTest.java | 8 +- .../shared/common/PageRequestTest.java | 84 +++++++++ .../de/effigenix/shared/common/PageTest.java | 73 ++++++++ .../shared/common/SortFieldTest.java | 83 +++++++++ .../inventory/InventoryCountListScreen.tsx | 19 +- .../components/inventory/StockListScreen.tsx | 19 +- .../inventory/StockMovementListScreen.tsx | 25 ++- .../inventory/StorageLocationListScreen.tsx | 23 ++- .../masterdata/articles/ArticleListScreen.tsx | 17 +- .../categories/CategoryListScreen.tsx | 17 +- .../customers/CustomerListScreen.tsx | 17 +- .../suppliers/SupplierListScreen.tsx | 17 +- .../components/production/BatchListScreen.tsx | 17 +- .../production/ProductionOrderListScreen.tsx | 26 ++- .../production/RecipeListScreen.tsx | 17 +- .../src/components/users/UserListScreen.tsx | 17 +- frontend/apps/cli/src/hooks/useArticles.ts | 45 ++++- frontend/apps/cli/src/hooks/useBatches.ts | 43 ++++- frontend/apps/cli/src/hooks/useCategories.ts | 45 ++++- frontend/apps/cli/src/hooks/useCustomers.ts | 45 ++++- .../apps/cli/src/hooks/useInventoryCounts.ts | 43 ++++- .../apps/cli/src/hooks/useProductionOrders.ts | 43 ++++- frontend/apps/cli/src/hooks/useRecipes.ts | 56 +++++- frontend/apps/cli/src/hooks/useRoles.ts | 42 ++++- .../apps/cli/src/hooks/useStockMovements.ts | 43 ++++- frontend/apps/cli/src/hooks/useStocks.ts | 43 ++++- .../apps/cli/src/hooks/useStorageLocations.ts | 45 ++++- frontend/apps/cli/src/hooks/useSuppliers.ts | 45 ++++- frontend/apps/cli/src/hooks/useUsers.ts | 45 ++++- .../api-client/src/resources/articles.ts | 10 +- .../api-client/src/resources/batches.ts | 11 +- .../api-client/src/resources/categories.ts | 10 +- .../api-client/src/resources/countries.ts | 13 +- .../api-client/src/resources/customers.ts | 10 +- .../src/resources/inventory-counts.ts | 11 +- .../src/resources/production-orders.ts | 11 +- .../api-client/src/resources/recipes.ts | 11 +- .../api-client/src/resources/roles.ts | 10 +- .../src/resources/stock-movements.ts | 11 +- .../api-client/src/resources/stocks.ts | 11 +- .../src/resources/storage-locations.ts | 11 +- .../api-client/src/resources/suppliers.ts | 10 +- .../api-client/src/resources/users.ts | 10 +- frontend/packages/types/src/common.ts | 15 +- justfile | 16 ++ .../LoadTestInfrastructure.java | 12 +- .../simulation/FullWorkloadSimulation.java | 99 +++++----- 151 files changed, 2880 insertions(+), 1120 deletions(-) create mode 100644 backend/src/main/java/de/effigenix/infrastructure/shared/persistence/PaginationHelper.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/shared/web/dto/PageResponse.java create mode 100644 backend/src/main/java/de/effigenix/shared/common/Page.java create mode 100644 backend/src/main/java/de/effigenix/shared/common/PageRequest.java create mode 100644 backend/src/main/java/de/effigenix/shared/common/SortDirection.java create mode 100644 backend/src/main/java/de/effigenix/shared/common/SortField.java create mode 100644 backend/src/test/java/de/effigenix/infrastructure/shared/persistence/PaginationHelperTest.java create mode 100644 backend/src/test/java/de/effigenix/shared/common/PageRequestTest.java create mode 100644 backend/src/test/java/de/effigenix/shared/common/PageTest.java create mode 100644 backend/src/test/java/de/effigenix/shared/common/SortFieldTest.java diff --git a/backend/src/main/java/de/effigenix/application/inventory/ListInventoryCounts.java b/backend/src/main/java/de/effigenix/application/inventory/ListInventoryCounts.java index 55e7471..a5f3756 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/ListInventoryCounts.java +++ b/backend/src/main/java/de/effigenix/application/inventory/ListInventoryCounts.java @@ -1,13 +1,13 @@ package de.effigenix.application.inventory; import de.effigenix.domain.inventory.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.AuthorizationPort; -import java.util.List; - public class ListInventoryCounts { private final InventoryCountRepository inventoryCountRepository; @@ -18,7 +18,7 @@ public class ListInventoryCounts { this.authPort = authPort; } - public Result> execute(String storageLocationId, String status, ActorId actorId) { + public Result> execute(String status, ActorId actorId, PageRequest pageRequest) { if (!authPort.can(actorId, InventoryAction.INVENTORY_COUNT_READ)) { return Result.failure(new InventoryCountError.Unauthorized("Not authorized to view inventory counts")); } @@ -32,34 +32,13 @@ public class ListInventoryCounts { } } - if (storageLocationId != null) { - StorageLocationId locId; - try { - locId = StorageLocationId.of(storageLocationId); - } catch (IllegalArgumentException e) { - return Result.failure(new InventoryCountError.InvalidStorageLocationId(e.getMessage())); - } - var result = mapResult(inventoryCountRepository.findByStorageLocationId(locId)); - if (parsedStatus != null) { - final InventoryCountStatus filterStatus = parsedStatus; - return result.map(counts -> counts.stream() - .filter(c -> c.status() == filterStatus) - .toList()); - } - return result; - } - - if (parsedStatus != null) { - return mapResult(inventoryCountRepository.findByStatus(parsedStatus)); - } - - return mapResult(inventoryCountRepository.findAll()); + return mapResult(inventoryCountRepository.findAll(parsedStatus, pageRequest)); } - private Result> mapResult(Result> result) { + private Result> mapResult(Result> result) { return switch (result) { case Result.Failure(var err) -> Result.failure(new InventoryCountError.RepositoryFailure(err.message())); - case Result.Success(var counts) -> Result.success(counts); + case Result.Success(var page) -> Result.success(page); }; } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/ListStockMovements.java b/backend/src/main/java/de/effigenix/application/inventory/ListStockMovements.java index 8ac9788..e0785dc 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/ListStockMovements.java +++ b/backend/src/main/java/de/effigenix/application/inventory/ListStockMovements.java @@ -7,13 +7,14 @@ import de.effigenix.domain.inventory.StockMovement; import de.effigenix.domain.inventory.StockMovementError; import de.effigenix.domain.inventory.StockMovementRepository; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.AuthorizationPort; import java.time.Instant; -import java.util.List; public class ListStockMovements { @@ -25,81 +26,62 @@ public class ListStockMovements { this.authPort = authPort; } - /** - * Lists stock movements with optional filtering. - * Filter priority (only one filter applied): stockId > articleId > batchReference > movementType > from/to. - * If multiple filters are provided, only the highest-priority filter is used. - */ - public Result> execute( + public Result> execute( String stockId, String articleId, String movementType, String batchReference, Instant from, Instant to, - ActorId performedBy) { + ActorId performedBy, PageRequest pageRequest) { if (!authPort.can(performedBy, InventoryAction.STOCK_MOVEMENT_READ)) { return Result.failure(new StockMovementError.Unauthorized("Not authorized to list stock movements")); } + // Validate filters + StockId sid = null; if (stockId != null) { - StockId sid; try { sid = StockId.of(stockId); } catch (IllegalArgumentException e) { return Result.failure(new StockMovementError.InvalidStockId(e.getMessage())); } - return mapResult(stockMovementRepository.findAllByStockId(sid)); } + ArticleId aid = null; if (articleId != null) { - ArticleId aid; try { aid = ArticleId.of(articleId); } catch (IllegalArgumentException e) { return Result.failure(new StockMovementError.InvalidArticleId(e.getMessage())); } - return mapResult(stockMovementRepository.findAllByArticleId(aid)); } - if (batchReference != null) { - if (batchReference.isBlank()) { - return Result.failure(new StockMovementError.InvalidBatchReference( - "Batch reference must not be blank")); - } - return mapResult(stockMovementRepository.findAllByBatchReference(batchReference)); + if (batchReference != null && batchReference.isBlank()) { + return Result.failure(new StockMovementError.InvalidBatchReference( + "Batch reference must not be blank")); } + MovementType type = null; if (movementType != null) { - MovementType type; try { type = MovementType.valueOf(movementType); } catch (IllegalArgumentException e) { return Result.failure(new StockMovementError.InvalidMovementType( "Invalid movement type: " + movementType)); } - return mapResult(stockMovementRepository.findAllByMovementType(type)); } - if (from != null || to != null) { - if (from != null && to != null && from.isAfter(to)) { - return Result.failure(new StockMovementError.InvalidDateRange( - "'from' must not be after 'to'")); - } - if (from != null && to != null) { - return mapResult(stockMovementRepository.findAllByPerformedAtBetween(from, to)); - } - if (from != null) { - return mapResult(stockMovementRepository.findAllByPerformedAtAfter(from)); - } - return mapResult(stockMovementRepository.findAllByPerformedAtBefore(to)); + if (from != null && to != null && from.isAfter(to)) { + return Result.failure(new StockMovementError.InvalidDateRange( + "'from' must not be after 'to'")); } - return mapResult(stockMovementRepository.findAll()); + return mapResult(stockMovementRepository.findAll(sid, aid, type, batchReference, from, to, pageRequest)); } - private Result> mapResult( - Result> result) { + private Result> mapResult( + Result> result) { return switch (result) { case Result.Failure(var err) -> Result.failure(new StockMovementError.RepositoryFailure(err.message())); - case Result.Success(var movements) -> Result.success(movements); + case Result.Success(var page) -> Result.success(page); }; } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/ListStocks.java b/backend/src/main/java/de/effigenix/application/inventory/ListStocks.java index b3787fe..f7486cf 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/ListStocks.java +++ b/backend/src/main/java/de/effigenix/application/inventory/ListStocks.java @@ -5,11 +5,11 @@ import de.effigenix.domain.inventory.StockError; import de.effigenix.domain.inventory.StockRepository; import de.effigenix.domain.inventory.StorageLocationId; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; -import java.util.List; - public class ListStocks { private final StockRepository stockRepository; @@ -18,7 +18,7 @@ public class ListStocks { this.stockRepository = stockRepository; } - public Result> execute(String storageLocationId, String articleId) { + public Result> execute(String storageLocationId, String articleId, PageRequest pageRequest) { if (storageLocationId != null && articleId != null) { return Result.failure(new StockError.InvalidFilterCombination( "Only one filter parameter allowed: storageLocationId or articleId")); @@ -31,7 +31,7 @@ public class ListStocks { } catch (IllegalArgumentException e) { return Result.failure(new StockError.InvalidStorageLocationId(e.getMessage())); } - return mapResult(stockRepository.findAllByStorageLocationId(locId)); + return mapResult(stockRepository.findAllByStorageLocationId(locId, pageRequest)); } if (articleId != null) { @@ -41,16 +41,16 @@ public class ListStocks { } catch (IllegalArgumentException e) { return Result.failure(new StockError.InvalidArticleId(e.getMessage())); } - return mapResult(stockRepository.findAllByArticleId(artId)); + return mapResult(stockRepository.findAllByArticleId(artId, pageRequest)); } - return mapResult(stockRepository.findAll()); + return mapResult(stockRepository.findAll(pageRequest)); } - private Result> mapResult(Result> result) { + private Result> mapResult(Result> result) { return switch (result) { case Result.Failure(var err) -> Result.failure(new StockError.RepositoryFailure(err.message())); - case Result.Success(var stocks) -> Result.success(stocks); + case Result.Success(var page) -> Result.success(page); }; } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/ListStorageLocations.java b/backend/src/main/java/de/effigenix/application/inventory/ListStorageLocations.java index a413989..a903d97 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/ListStorageLocations.java +++ b/backend/src/main/java/de/effigenix/application/inventory/ListStorageLocations.java @@ -1,11 +1,11 @@ package de.effigenix.application.inventory; import de.effigenix.domain.inventory.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; -import java.util.List; - public class ListStorageLocations { private final StorageLocationRepository storageLocationRepository; @@ -14,40 +14,24 @@ public class ListStorageLocations { this.storageLocationRepository = storageLocationRepository; } - public Result> execute(String storageType, Boolean active) { + public Result> execute(String storageType, Boolean active, PageRequest pageRequest) { + StorageType type = null; if (storageType != null) { - return findByStorageType(storageType, active); - } - return mapResult(storageLocationRepository.findAll()) - .map(locations -> filterByActive(locations, active)); - } - - private Result> findByStorageType(String storageType, Boolean active) { - StorageType type; - try { - type = StorageType.valueOf(storageType); - } catch (IllegalArgumentException e) { - return Result.failure(new StorageLocationError.InvalidStorageType(storageType)); + try { + type = StorageType.valueOf(storageType); + } catch (IllegalArgumentException e) { + return Result.failure(new StorageLocationError.InvalidStorageType(storageType)); + } } - return mapResult(storageLocationRepository.findByStorageType(type)) - .map(locations -> filterByActive(locations, active)); + return mapResult(storageLocationRepository.findAll(type, active, pageRequest)); } - private List filterByActive(List locations, Boolean active) { - if (active == null) { - return locations; - } - return locations.stream() - .filter(loc -> loc.active() == active) - .toList(); - } - - private Result> mapResult( - Result> result) { + private Result> mapResult( + Result> result) { return switch (result) { case Result.Failure(var err) -> Result.failure(new StorageLocationError.RepositoryFailure(err.message())); - case Result.Success(var locations) -> Result.success(locations); + case Result.Success(var page) -> Result.success(page); }; } } diff --git a/backend/src/main/java/de/effigenix/application/masterdata/article/ListArticles.java b/backend/src/main/java/de/effigenix/application/masterdata/article/ListArticles.java index 26da2a6..25d4160 100644 --- a/backend/src/main/java/de/effigenix/application/masterdata/article/ListArticles.java +++ b/backend/src/main/java/de/effigenix/application/masterdata/article/ListArticles.java @@ -4,13 +4,10 @@ import de.effigenix.domain.masterdata.article.Article; import de.effigenix.domain.masterdata.article.ArticleError; import de.effigenix.domain.masterdata.article.ArticleRepository; import de.effigenix.domain.masterdata.article.ArticleStatus; -import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; -import java.util.List; - -import static de.effigenix.shared.common.Result.*; - public class ListArticles { private final ArticleRepository articleRepository; @@ -19,30 +16,12 @@ public class ListArticles { this.articleRepository = articleRepository; } - public Result> execute() { - return switch (articleRepository.findAll()) { - case Failure(var err) -> + public Result> execute(ArticleStatus status, PageRequest pageRequest) { + return switch (articleRepository.findAll(status, pageRequest)) { + case Result.Failure(var err) -> Result.failure(new ArticleError.RepositoryFailure(err.message())); - case Success(var articles) -> - Result.success(articles); - }; - } - - public Result> executeByCategory(ProductCategoryId categoryId) { - return switch (articleRepository.findByCategory(categoryId)) { - case Failure(var err) -> - Result.failure(new ArticleError.RepositoryFailure(err.message())); - case Success(var articles) -> - Result.success(articles); - }; - } - - public Result> executeByStatus(ArticleStatus status) { - return switch (articleRepository.findByStatus(status)) { - case Failure(var err) -> - Result.failure(new ArticleError.RepositoryFailure(err.message())); - case Success(var articles) -> - Result.success(articles); + case Result.Success(var page) -> + Result.success(page); }; } } diff --git a/backend/src/main/java/de/effigenix/application/masterdata/customer/ListCustomers.java b/backend/src/main/java/de/effigenix/application/masterdata/customer/ListCustomers.java index 082b52d..9687837 100644 --- a/backend/src/main/java/de/effigenix/application/masterdata/customer/ListCustomers.java +++ b/backend/src/main/java/de/effigenix/application/masterdata/customer/ListCustomers.java @@ -1,12 +1,10 @@ package de.effigenix.application.masterdata.customer; import de.effigenix.domain.masterdata.customer.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; -import java.util.List; - -import static de.effigenix.shared.common.Result.*; - public class ListCustomers { private final CustomerRepository customerRepository; @@ -15,30 +13,12 @@ public class ListCustomers { this.customerRepository = customerRepository; } - public Result> execute() { - return switch (customerRepository.findAll()) { - case Failure(var err) -> + public Result> execute(PageRequest pageRequest) { + return switch (customerRepository.findAll(pageRequest)) { + case Result.Failure(var err) -> Result.failure(new CustomerError.RepositoryFailure(err.message())); - case Success(var customers) -> - Result.success(customers); - }; - } - - public Result> executeByType(CustomerType type) { - return switch (customerRepository.findByType(type)) { - case Failure(var err) -> - Result.failure(new CustomerError.RepositoryFailure(err.message())); - case Success(var customers) -> - Result.success(customers); - }; - } - - public Result> executeByStatus(CustomerStatus status) { - return switch (customerRepository.findByStatus(status)) { - case Failure(var err) -> - Result.failure(new CustomerError.RepositoryFailure(err.message())); - case Success(var customers) -> - Result.success(customers); + case Result.Success(var page) -> + Result.success(page); }; } } diff --git a/backend/src/main/java/de/effigenix/application/masterdata/productcategory/ListProductCategories.java b/backend/src/main/java/de/effigenix/application/masterdata/productcategory/ListProductCategories.java index 3b20a25..5762084 100644 --- a/backend/src/main/java/de/effigenix/application/masterdata/productcategory/ListProductCategories.java +++ b/backend/src/main/java/de/effigenix/application/masterdata/productcategory/ListProductCategories.java @@ -3,12 +3,10 @@ package de.effigenix.application.masterdata.productcategory; import de.effigenix.domain.masterdata.productcategory.ProductCategory; import de.effigenix.domain.masterdata.productcategory.ProductCategoryError; import de.effigenix.domain.masterdata.productcategory.ProductCategoryRepository; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; -import java.util.List; - -import static de.effigenix.shared.common.Result.*; - public class ListProductCategories { private final ProductCategoryRepository categoryRepository; @@ -17,12 +15,12 @@ public class ListProductCategories { this.categoryRepository = categoryRepository; } - public Result> execute() { - return switch (categoryRepository.findAll()) { - case Failure(var err) -> + public Result> execute(PageRequest pageRequest) { + return switch (categoryRepository.findAll(pageRequest)) { + case Result.Failure(var err) -> Result.failure(new ProductCategoryError.RepositoryFailure(err.message())); - case Success(var categories) -> - Result.success(categories); + case Result.Success(var page) -> + Result.success(page); }; } } diff --git a/backend/src/main/java/de/effigenix/application/masterdata/supplier/ListSuppliers.java b/backend/src/main/java/de/effigenix/application/masterdata/supplier/ListSuppliers.java index bd39bc1..ce4501a 100644 --- a/backend/src/main/java/de/effigenix/application/masterdata/supplier/ListSuppliers.java +++ b/backend/src/main/java/de/effigenix/application/masterdata/supplier/ListSuppliers.java @@ -4,12 +4,10 @@ import de.effigenix.domain.masterdata.supplier.Supplier; import de.effigenix.domain.masterdata.supplier.SupplierError; import de.effigenix.domain.masterdata.supplier.SupplierRepository; import de.effigenix.domain.masterdata.supplier.SupplierStatus; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; -import java.util.List; - -import static de.effigenix.shared.common.Result.*; - public class ListSuppliers { private final SupplierRepository supplierRepository; @@ -18,21 +16,12 @@ public class ListSuppliers { this.supplierRepository = supplierRepository; } - public Result> execute() { - return switch (supplierRepository.findAll()) { - case Failure(var err) -> + public Result> execute(SupplierStatus status, PageRequest pageRequest) { + return switch (supplierRepository.findAll(status, pageRequest)) { + case Result.Failure(var err) -> Result.failure(new SupplierError.RepositoryFailure(err.message())); - case Success(var suppliers) -> - Result.success(suppliers); - }; - } - - public Result> executeByStatus(SupplierStatus status) { - return switch (supplierRepository.findByStatus(status)) { - case Failure(var err) -> - Result.failure(new SupplierError.RepositoryFailure(err.message())); - case Success(var suppliers) -> - Result.success(suppliers); + case Result.Success(var page) -> + Result.success(page); }; } } diff --git a/backend/src/main/java/de/effigenix/application/production/ListBatches.java b/backend/src/main/java/de/effigenix/application/production/ListBatches.java index 59662cd..bfbae52 100644 --- a/backend/src/main/java/de/effigenix/application/production/ListBatches.java +++ b/backend/src/main/java/de/effigenix/application/production/ListBatches.java @@ -1,6 +1,8 @@ package de.effigenix.application.production; import de.effigenix.domain.production.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.AuthorizationPort; @@ -21,16 +23,16 @@ public class ListBatches { this.authorizationPort = authorizationPort; } - public Result> execute(ActorId performedBy) { + public Result> execute(ActorId performedBy, PageRequest pageRequest) { if (!authorizationPort.can(performedBy, ProductionAction.BATCH_READ)) { return Result.failure(new BatchError.Unauthorized("Not authorized to read batches")); } - switch (batchRepository.findAllSummary()) { + switch (batchRepository.findAllSummary(pageRequest)) { case Result.Failure(var err) -> { return Result.failure(new BatchError.RepositoryFailure(err.message())); } - case Result.Success(var batches) -> - { return Result.success(batches); } + case Result.Success(var page) -> + { return Result.success(page); } } } diff --git a/backend/src/main/java/de/effigenix/application/production/ListProductionOrders.java b/backend/src/main/java/de/effigenix/application/production/ListProductionOrders.java index a7e1aae..b211864 100644 --- a/backend/src/main/java/de/effigenix/application/production/ListProductionOrders.java +++ b/backend/src/main/java/de/effigenix/application/production/ListProductionOrders.java @@ -1,6 +1,8 @@ package de.effigenix.application.production; import de.effigenix.domain.production.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; @@ -22,12 +24,15 @@ public class ListProductionOrders { this.authorizationPort = authorizationPort; } - public Result> execute(ActorId performedBy) { + public Result> execute(ActorId performedBy, PageRequest pageRequest) { if (!authorizationPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)) { return Result.failure(new ProductionOrderError.Unauthorized("Not authorized to list production orders")); } - return wrapResult(productionOrderRepository.findAll()); + return switch (productionOrderRepository.findAll(pageRequest)) { + case Result.Failure(var err) -> Result.failure(new ProductionOrderError.RepositoryFailure(err.message())); + case Result.Success(var page) -> Result.success(page); + }; } public Result> executeByDateRange(LocalDate from, LocalDate to, ActorId performedBy) { diff --git a/backend/src/main/java/de/effigenix/application/production/ListRecipes.java b/backend/src/main/java/de/effigenix/application/production/ListRecipes.java index 8e8be26..d5e8a28 100644 --- a/backend/src/main/java/de/effigenix/application/production/ListRecipes.java +++ b/backend/src/main/java/de/effigenix/application/production/ListRecipes.java @@ -1,6 +1,8 @@ package de.effigenix.application.production; import de.effigenix.domain.production.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.AuthorizationPort; @@ -17,16 +19,16 @@ public class ListRecipes { this.authorizationPort = authorizationPort; } - public Result> execute(ActorId performedBy) { + public Result> execute(ActorId performedBy, PageRequest pageRequest) { if (!authorizationPort.can(performedBy, ProductionAction.RECIPE_READ)) { return Result.failure(new RecipeError.Unauthorized("Not authorized to read recipes")); } - switch (recipeRepository.findAll()) { + switch (recipeRepository.findAll(pageRequest)) { case Result.Failure(var err) -> { return Result.failure(new RecipeError.RepositoryFailure(err.message())); } - case Result.Success(var recipes) -> - { return Result.success(recipes); } + case Result.Success(var page) -> + { return Result.success(page); } } } diff --git a/backend/src/main/java/de/effigenix/application/shared/ListCountries.java b/backend/src/main/java/de/effigenix/application/shared/ListCountries.java index d43b121..52350a8 100644 --- a/backend/src/main/java/de/effigenix/application/shared/ListCountries.java +++ b/backend/src/main/java/de/effigenix/application/shared/ListCountries.java @@ -2,6 +2,8 @@ package de.effigenix.application.shared; import de.effigenix.shared.common.Country; import de.effigenix.shared.common.CountryRepository; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import java.util.List; @@ -13,7 +15,11 @@ public class ListCountries { this.countryRepository = countryRepository; } - public List execute(String query) { + public Page execute(PageRequest pageRequest) { + return countryRepository.findAll(pageRequest); + } + + public List search(String query) { if (query == null || query.isBlank()) { return countryRepository.findAll(); } diff --git a/backend/src/main/java/de/effigenix/application/usermanagement/ListUsers.java b/backend/src/main/java/de/effigenix/application/usermanagement/ListUsers.java index bfd7345..325d874 100644 --- a/backend/src/main/java/de/effigenix/application/usermanagement/ListUsers.java +++ b/backend/src/main/java/de/effigenix/application/usermanagement/ListUsers.java @@ -4,13 +4,11 @@ import de.effigenix.application.usermanagement.dto.UserDTO; import de.effigenix.domain.usermanagement.UserError; import de.effigenix.domain.usermanagement.UserManagementAction; import de.effigenix.domain.usermanagement.UserRepository; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.AuthorizationPort; -import de.effigenix.shared.security.BranchId; - -import java.util.List; -import java.util.stream.Collectors; /** * Use Case: List all users (with optional branch filtering). @@ -25,29 +23,13 @@ public class ListUsers { this.authPort = authPort; } - /** - * Lists all users (admin view). - */ - public Result> execute(ActorId performedBy) { + public Result> execute(String branchId, ActorId performedBy, PageRequest pageRequest) { if (!authPort.can(performedBy, UserManagementAction.USER_LIST)) { return Result.failure(new UserError.Unauthorized("Not authorized to list users")); } - return userRepository.findAll() + return userRepository.findAll(branchId, pageRequest) .mapError(err -> (UserError) new UserError.RepositoryFailure(err.message())) - .map(users -> users.stream().map(UserDTO::from).collect(Collectors.toList())); - } - - /** - * Lists users for a specific branch (filtered view). - */ - public Result> executeForBranch(BranchId branchId, ActorId performedBy) { - if (!authPort.can(performedBy, UserManagementAction.USER_LIST)) { - return Result.failure(new UserError.Unauthorized("Not authorized to list users")); - } - - return userRepository.findByBranchId(branchId.value()) - .mapError(err -> (UserError) new UserError.RepositoryFailure(err.message())) - .map(users -> users.stream().map(UserDTO::from).collect(Collectors.toList())); + .map(page -> page.map(UserDTO::from)); } } diff --git a/backend/src/main/java/de/effigenix/domain/inventory/InventoryCountRepository.java b/backend/src/main/java/de/effigenix/domain/inventory/InventoryCountRepository.java index 0b5509f..f0d57e6 100644 --- a/backend/src/main/java/de/effigenix/domain/inventory/InventoryCountRepository.java +++ b/backend/src/main/java/de/effigenix/domain/inventory/InventoryCountRepository.java @@ -1,5 +1,7 @@ package de.effigenix.domain.inventory; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -10,6 +12,8 @@ public interface InventoryCountRepository { Result> findById(InventoryCountId id); + Result> findAll(InventoryCountStatus status, PageRequest pageRequest); + Result> findAll(); Result> findByStorageLocationId(StorageLocationId storageLocationId); diff --git a/backend/src/main/java/de/effigenix/domain/inventory/StockMovementRepository.java b/backend/src/main/java/de/effigenix/domain/inventory/StockMovementRepository.java index 1188064..01e25b2 100644 --- a/backend/src/main/java/de/effigenix/domain/inventory/StockMovementRepository.java +++ b/backend/src/main/java/de/effigenix/domain/inventory/StockMovementRepository.java @@ -1,6 +1,8 @@ package de.effigenix.domain.inventory; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -12,6 +14,11 @@ public interface StockMovementRepository { Result> findById(StockMovementId id); + Result> findAll(StockId stockId, ArticleId articleId, + MovementType movementType, String batchRef, + Instant from, Instant to, + PageRequest pageRequest); + Result> findAll(); Result> findAllByStockId(StockId stockId); diff --git a/backend/src/main/java/de/effigenix/domain/inventory/StockRepository.java b/backend/src/main/java/de/effigenix/domain/inventory/StockRepository.java index e225962..e52093e 100644 --- a/backend/src/main/java/de/effigenix/domain/inventory/StockRepository.java +++ b/backend/src/main/java/de/effigenix/domain/inventory/StockRepository.java @@ -1,6 +1,8 @@ package de.effigenix.domain.inventory; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -12,6 +14,12 @@ public interface StockRepository { Result> findById(StockId id); + Result> findAll(PageRequest pageRequest); + + Result> findAllByStorageLocationId(StorageLocationId storageLocationId, PageRequest pageRequest); + + Result> findAllByArticleId(ArticleId articleId, PageRequest pageRequest); + Result> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId); Result existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId); @@ -26,5 +34,7 @@ public interface StockRepository { Result> findAllBelowMinimumLevel(); + Result> findAllByBatchId(String batchId); + Result save(Stock stock); } diff --git a/backend/src/main/java/de/effigenix/domain/inventory/StorageLocationRepository.java b/backend/src/main/java/de/effigenix/domain/inventory/StorageLocationRepository.java index 4d6be55..f4d9b3c 100644 --- a/backend/src/main/java/de/effigenix/domain/inventory/StorageLocationRepository.java +++ b/backend/src/main/java/de/effigenix/domain/inventory/StorageLocationRepository.java @@ -1,5 +1,7 @@ package de.effigenix.domain.inventory; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -10,6 +12,8 @@ public interface StorageLocationRepository { Result> findById(StorageLocationId id); + Result> findAll(StorageType storageType, Boolean active, PageRequest pageRequest); + Result> findAll(); Result> findByStorageType(StorageType storageType); diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/article/ArticleRepository.java b/backend/src/main/java/de/effigenix/domain/masterdata/article/ArticleRepository.java index c843669..a10ce04 100644 --- a/backend/src/main/java/de/effigenix/domain/masterdata/article/ArticleRepository.java +++ b/backend/src/main/java/de/effigenix/domain/masterdata/article/ArticleRepository.java @@ -1,6 +1,8 @@ package de.effigenix.domain.masterdata.article; import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -11,6 +13,8 @@ public interface ArticleRepository { Result> findById(ArticleId id); + Result> findAll(ArticleStatus status, PageRequest pageRequest); + Result> findAll(); Result> findByCategory(ProductCategoryId categoryId); diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/customer/CustomerRepository.java b/backend/src/main/java/de/effigenix/domain/masterdata/customer/CustomerRepository.java index d8afb3f..d6506cf 100644 --- a/backend/src/main/java/de/effigenix/domain/masterdata/customer/CustomerRepository.java +++ b/backend/src/main/java/de/effigenix/domain/masterdata/customer/CustomerRepository.java @@ -1,5 +1,7 @@ package de.effigenix.domain.masterdata.customer; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -10,6 +12,8 @@ public interface CustomerRepository { Result> findById(CustomerId id); + Result> findAll(PageRequest pageRequest); + Result> findAll(); Result> findByType(CustomerType type); diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/productcategory/ProductCategoryRepository.java b/backend/src/main/java/de/effigenix/domain/masterdata/productcategory/ProductCategoryRepository.java index 3703232..2a63656 100644 --- a/backend/src/main/java/de/effigenix/domain/masterdata/productcategory/ProductCategoryRepository.java +++ b/backend/src/main/java/de/effigenix/domain/masterdata/productcategory/ProductCategoryRepository.java @@ -1,5 +1,7 @@ package de.effigenix.domain.masterdata.productcategory; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -10,6 +12,8 @@ public interface ProductCategoryRepository { Result> findById(ProductCategoryId id); + Result> findAll(PageRequest pageRequest); + Result> findAll(); Result save(ProductCategory category); diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/supplier/SupplierRepository.java b/backend/src/main/java/de/effigenix/domain/masterdata/supplier/SupplierRepository.java index bc59204..427c042 100644 --- a/backend/src/main/java/de/effigenix/domain/masterdata/supplier/SupplierRepository.java +++ b/backend/src/main/java/de/effigenix/domain/masterdata/supplier/SupplierRepository.java @@ -1,5 +1,7 @@ package de.effigenix.domain.masterdata.supplier; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -10,6 +12,8 @@ public interface SupplierRepository { Result> findById(SupplierId id); + Result> findAll(SupplierStatus status, PageRequest pageRequest); + Result> findAll(); Result> findByStatus(SupplierStatus status); diff --git a/backend/src/main/java/de/effigenix/domain/production/BatchRepository.java b/backend/src/main/java/de/effigenix/domain/production/BatchRepository.java index 5f4aef0..9bc412b 100644 --- a/backend/src/main/java/de/effigenix/domain/production/BatchRepository.java +++ b/backend/src/main/java/de/effigenix/domain/production/BatchRepository.java @@ -1,5 +1,7 @@ package de.effigenix.domain.production; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -11,6 +13,8 @@ public interface BatchRepository { Result> findById(BatchId id); + Result> findAllSummary(PageRequest pageRequest); + Result> findAll(); Result> findByBatchNumber(BatchNumber batchNumber); diff --git a/backend/src/main/java/de/effigenix/domain/production/ProductionOrderRepository.java b/backend/src/main/java/de/effigenix/domain/production/ProductionOrderRepository.java index 19821ab..82e8d20 100644 --- a/backend/src/main/java/de/effigenix/domain/production/ProductionOrderRepository.java +++ b/backend/src/main/java/de/effigenix/domain/production/ProductionOrderRepository.java @@ -1,5 +1,7 @@ package de.effigenix.domain.production; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -11,6 +13,8 @@ public interface ProductionOrderRepository { Result> findById(ProductionOrderId id); + Result> findAll(PageRequest pageRequest); + Result> findAll(); Result> findByDateRange(LocalDate from, LocalDate to); diff --git a/backend/src/main/java/de/effigenix/domain/production/RecipeRepository.java b/backend/src/main/java/de/effigenix/domain/production/RecipeRepository.java index dea2779..edbc264 100644 --- a/backend/src/main/java/de/effigenix/domain/production/RecipeRepository.java +++ b/backend/src/main/java/de/effigenix/domain/production/RecipeRepository.java @@ -1,5 +1,7 @@ package de.effigenix.domain.production; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -10,6 +12,8 @@ public interface RecipeRepository { Result> findById(RecipeId id); + Result> findAll(PageRequest pageRequest); + Result> findAll(); Result save(Recipe recipe); diff --git a/backend/src/main/java/de/effigenix/domain/usermanagement/RoleRepository.java b/backend/src/main/java/de/effigenix/domain/usermanagement/RoleRepository.java index 6db3552..6165ae8 100644 --- a/backend/src/main/java/de/effigenix/domain/usermanagement/RoleRepository.java +++ b/backend/src/main/java/de/effigenix/domain/usermanagement/RoleRepository.java @@ -1,5 +1,7 @@ package de.effigenix.domain.usermanagement; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -14,6 +16,8 @@ public interface RoleRepository { Result> findById(RoleId id); + Result> findAll(PageRequest pageRequest); + Result> findByName(RoleName name); Result> findAll(); diff --git a/backend/src/main/java/de/effigenix/domain/usermanagement/UserRepository.java b/backend/src/main/java/de/effigenix/domain/usermanagement/UserRepository.java index 37092c1..871b186 100644 --- a/backend/src/main/java/de/effigenix/domain/usermanagement/UserRepository.java +++ b/backend/src/main/java/de/effigenix/domain/usermanagement/UserRepository.java @@ -1,5 +1,7 @@ package de.effigenix.domain.usermanagement; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -14,6 +16,8 @@ public interface UserRepository { Result> findById(UserId id); + Result> findAll(String branchId, PageRequest pageRequest); + Result> findByUsername(String username); Result> findByEmail(String email); diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcInventoryCountRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcInventoryCountRepository.java index 4eb584c..ae27a83 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcInventoryCountRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcInventoryCountRepository.java @@ -2,6 +2,9 @@ package de.effigenix.infrastructure.inventory.persistence.repository; import de.effigenix.domain.inventory.*; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -19,6 +22,7 @@ import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.List; +import java.util.Map; import java.util.Optional; @Repository @@ -27,6 +31,12 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository { private static final Logger logger = LoggerFactory.getLogger(JdbcInventoryCountRepository.class); + private static final Map SORT_FIELD_MAP = Map.of( + "createdAt", "created_at", + "status", "status", + "countDate", "count_date" + ); + private final JdbcClient jdbc; public JdbcInventoryCountRepository(JdbcClient jdbc) { @@ -50,6 +60,33 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository { } } + @Override + public Result> findAll(InventoryCountStatus status, PageRequest pageRequest) { + try { + String where = ""; + var params = new java.util.LinkedHashMap(); + if (status != null) { + where = " WHERE status = :status"; + params.put("status", status.name()); + } + long total = jdbc.sql("SELECT COUNT(*) FROM inventory_counts" + where) + .params(params) + .query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + if (orderBy.isEmpty()) orderBy = " ORDER BY created_at DESC"; + params.put("limit", pageRequest.size()); + params.put("offset", pageRequest.offset()); + var counts = jdbc.sql("SELECT * FROM inventory_counts" + where + orderBy + " LIMIT :limit OFFSET :offset") + .params(params) + .query(this::mapCountRow) + .list(); + return Result.success(Page.of(loadChildrenForAll(counts), pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.warn("Database error in findAll(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findAll() { try { diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockMovementRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockMovementRepository.java index dbb46ee..a00221c 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockMovementRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockMovementRepository.java @@ -2,6 +2,9 @@ package de.effigenix.infrastructure.inventory.persistence.repository; import de.effigenix.domain.inventory.*; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -18,6 +21,7 @@ import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.List; +import java.util.Map; import java.util.Optional; @Repository @@ -26,6 +30,12 @@ public class JdbcStockMovementRepository implements StockMovementRepository { private static final Logger logger = LoggerFactory.getLogger(JdbcStockMovementRepository.class); + private static final Map SORT_FIELD_MAP = Map.of( + "performedAt", "performed_at", + "movementType", "movement_type", + "articleId", "article_id" + ); + private final JdbcClient jdbc; public JdbcStockMovementRepository(JdbcClient jdbc) { @@ -46,6 +56,61 @@ public class JdbcStockMovementRepository implements StockMovementRepository { } } + @Override + public Result> findAll(StockId stockId, ArticleId articleId, + MovementType movementType, String batchRef, + Instant from, Instant to, + PageRequest pageRequest) { + try { + var where = new StringBuilder(); + var params = new java.util.LinkedHashMap(); + if (stockId != null) { + where.append(" WHERE stock_id = :stockId"); + params.put("stockId", stockId.value()); + } + if (articleId != null) { + where.append(where.isEmpty() ? " WHERE " : " AND "); + where.append("article_id = :articleId"); + params.put("articleId", articleId.value()); + } + if (movementType != null) { + where.append(where.isEmpty() ? " WHERE " : " AND "); + where.append("movement_type = :movementType"); + params.put("movementType", movementType.name()); + } + if (batchRef != null) { + where.append(where.isEmpty() ? " WHERE " : " AND "); + where.append("batch_id = :batchRef"); + params.put("batchRef", batchRef); + } + if (from != null) { + where.append(where.isEmpty() ? " WHERE " : " AND "); + where.append("performed_at >= :from"); + params.put("from", from.atOffset(java.time.ZoneOffset.UTC)); + } + if (to != null) { + where.append(where.isEmpty() ? " WHERE " : " AND "); + where.append("performed_at <= :to"); + params.put("to", to.atOffset(java.time.ZoneOffset.UTC)); + } + long total = jdbc.sql("SELECT COUNT(*) FROM stock_movements" + where) + .params(params) + .query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + if (orderBy.isEmpty()) orderBy = " ORDER BY performed_at DESC"; + params.put("limit", pageRequest.size()); + params.put("offset", pageRequest.offset()); + var items = jdbc.sql("SELECT * FROM stock_movements" + where + orderBy + " LIMIT :limit OFFSET :offset") + .params(params) + .query(this::mapRow) + .list(); + return Result.success(Page.of(items, pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAll(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findAll() { try { diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockRepository.java index 930965f..386ef51 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockRepository.java @@ -2,6 +2,9 @@ package de.effigenix.infrastructure.inventory.persistence.repository; import de.effigenix.domain.inventory.*; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -20,6 +23,7 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; @Repository @@ -28,6 +32,13 @@ public class JdbcStockRepository implements StockRepository { private static final Logger logger = LoggerFactory.getLogger(JdbcStockRepository.class); + private static final int SAFETY_LIMIT = 500; + + private static final Map SORT_FIELD_MAP = Map.of( + "articleId", "article_id", + "storageLocationId", "storage_location_id" + ); + private final JdbcClient jdbc; public JdbcStockRepository(JdbcClient jdbc) { @@ -51,6 +62,63 @@ public class JdbcStockRepository implements StockRepository { } } + @Override + public Result> findAll(PageRequest pageRequest) { + try { + long total = jdbc.sql("SELECT COUNT(*) FROM stocks").query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + var stocks = jdbc.sql("SELECT * FROM stocks" + orderBy + " LIMIT :limit OFFSET :offset") + .param("limit", pageRequest.size()) + .param("offset", pageRequest.offset()) + .query(this::mapStockRow) + .list(); + return Result.success(Page.of(loadChildrenForAll(stocks), pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAll(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result> findAllByStorageLocationId(StorageLocationId storageLocationId, PageRequest pageRequest) { + try { + long total = jdbc.sql("SELECT COUNT(*) FROM stocks WHERE storage_location_id = :storageLocationId") + .param("storageLocationId", storageLocationId.value()) + .query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + var stocks = jdbc.sql("SELECT * FROM stocks WHERE storage_location_id = :storageLocationId" + orderBy + " LIMIT :limit OFFSET :offset") + .param("storageLocationId", storageLocationId.value()) + .param("limit", pageRequest.size()) + .param("offset", pageRequest.offset()) + .query(this::mapStockRow) + .list(); + return Result.success(Page.of(loadChildrenForAll(stocks), pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAllByStorageLocationId(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result> findAllByArticleId(ArticleId articleId, PageRequest pageRequest) { + try { + long total = jdbc.sql("SELECT COUNT(*) FROM stocks WHERE article_id = :articleId") + .param("articleId", articleId.value()) + .query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + var stocks = jdbc.sql("SELECT * FROM stocks WHERE article_id = :articleId" + orderBy + " LIMIT :limit OFFSET :offset") + .param("articleId", articleId.value()) + .param("limit", pageRequest.size()) + .param("offset", pageRequest.offset()) + .query(this::mapStockRow) + .list(); + return Result.success(Page.of(loadChildrenForAll(stocks), pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAllByArticleId(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { try { @@ -129,18 +197,24 @@ public class JdbcStockRepository implements StockRepository { @Override public Result> findAllWithExpiryRelevantBatches(LocalDate referenceDate) { try { - var stocks = jdbc.sql(""" + var items = new ArrayList<>(jdbc.sql(""" SELECT DISTINCT s.* FROM stocks s JOIN stock_batches b ON b.stock_id = s.id WHERE (b.status IN ('AVAILABLE', 'EXPIRING_SOON') AND b.expiry_date < :today) OR (s.minimum_shelf_life_days IS NOT NULL AND b.status = 'AVAILABLE' AND b.expiry_date >= :today AND b.expiry_date < :today + s.minimum_shelf_life_days * INTERVAL '1 day') + LIMIT :limit """) .param("today", referenceDate) + .param("limit", SAFETY_LIMIT + 1) .query(this::mapStockRow) - .list(); - return Result.success(loadChildrenForAll(stocks)); + .list()); + if (items.size() > SAFETY_LIMIT) { + logger.warn("Safety limit reached for findAllWithExpiryRelevantBatches: {} results (limit: {})", items.size(), SAFETY_LIMIT); + items = new ArrayList<>(items.subList(0, SAFETY_LIMIT)); + } + return Result.success(loadChildrenForAll(items)); } catch (Exception e) { logger.trace("Database error in findAllWithExpiryRelevantBatches", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); @@ -150,18 +224,42 @@ public class JdbcStockRepository implements StockRepository { @Override public Result> findAllBelowMinimumLevel() { try { - var stocks = jdbc.sql(""" + var items = new ArrayList<>(jdbc.sql(""" SELECT DISTINCT s.* FROM stocks s LEFT JOIN stock_batches b ON b.stock_id = s.id AND b.status IN ('AVAILABLE', 'EXPIRING_SOON') WHERE s.minimum_level_amount IS NOT NULL GROUP BY s.id HAVING COALESCE(SUM(b.quantity_amount), 0) < s.minimum_level_amount + LIMIT :limit """) + .param("limit", SAFETY_LIMIT + 1) + .query(this::mapStockRow) + .list()); + if (items.size() > SAFETY_LIMIT) { + logger.warn("Safety limit reached for findAllBelowMinimumLevel: {} results (limit: {})", items.size(), SAFETY_LIMIT); + items = new ArrayList<>(items.subList(0, SAFETY_LIMIT)); + } + return Result.success(loadChildrenForAll(items)); + } catch (Exception e) { + logger.trace("Database error in findAllBelowMinimumLevel", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result> findAllByBatchId(String batchId) { + try { + var stocks = jdbc.sql(""" + SELECT DISTINCT s.* FROM stocks s + JOIN stock_batches b ON b.stock_id = s.id + WHERE b.batch_id = :batchId + """) + .param("batchId", batchId) .query(this::mapStockRow) .list(); return Result.success(loadChildrenForAll(stocks)); } catch (Exception e) { - logger.trace("Database error in findAllBelowMinimumLevel", e); + logger.trace("Database error in findAllByBatchId", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStorageLocationRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStorageLocationRepository.java index 3c7073f..b07bd45 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStorageLocationRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStorageLocationRepository.java @@ -1,6 +1,9 @@ package de.effigenix.infrastructure.inventory.persistence.repository; import de.effigenix.domain.inventory.*; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.slf4j.Logger; @@ -13,6 +16,7 @@ import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import java.util.Map; import java.util.Optional; @Repository @@ -21,6 +25,12 @@ public class JdbcStorageLocationRepository implements StorageLocationRepository private static final Logger logger = LoggerFactory.getLogger(JdbcStorageLocationRepository.class); + private static final Map SORT_FIELD_MAP = Map.of( + "name", "name", + "storageType", "storage_type", + "active", "active" + ); + private final JdbcClient jdbc; public JdbcStorageLocationRepository(JdbcClient jdbc) { @@ -41,6 +51,38 @@ public class JdbcStorageLocationRepository implements StorageLocationRepository } } + @Override + public Result> findAll(StorageType storageType, Boolean active, PageRequest pageRequest) { + try { + var where = new StringBuilder(); + var params = new java.util.LinkedHashMap(); + if (storageType != null) { + where.append(" WHERE storage_type = :storageType"); + params.put("storageType", storageType.name()); + } + if (active != null) { + where.append(where.isEmpty() ? " WHERE " : " AND "); + where.append("active = :active"); + params.put("active", active); + } + long total = jdbc.sql("SELECT COUNT(*) FROM storage_locations" + where) + .params(params) + .query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + if (orderBy.isEmpty()) orderBy = " ORDER BY name"; + params.put("limit", pageRequest.size()); + params.put("offset", pageRequest.offset()); + var items = jdbc.sql("SELECT * FROM storage_locations" + where + orderBy + " LIMIT :limit OFFSET :offset") + .params(params) + .query(this::mapRow) + .list(); + return Result.success(Page.of(items, pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAll(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findAll() { try { diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/InventoryCountController.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/InventoryCountController.java index 25c773f..5802bd3 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/InventoryCountController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/InventoryCountController.java @@ -16,7 +16,10 @@ import de.effigenix.infrastructure.inventory.web.dto.CancelInventoryCountRequest import de.effigenix.infrastructure.inventory.web.dto.CreateInventoryCountRequest; import de.effigenix.infrastructure.inventory.web.dto.InventoryCountResponse; import de.effigenix.infrastructure.inventory.web.dto.RecordCountItemRequest; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; +import de.effigenix.shared.common.SortField; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.UserLookupPort; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -96,19 +99,19 @@ public class InventoryCountController { @GetMapping @PreAuthorize("hasAuthority('INVENTORY_COUNT_READ')") - public ResponseEntity> listInventoryCounts( - @RequestParam(required = false) String storageLocationId, + public ResponseEntity> listInventoryCounts( @RequestParam(required = false) String status, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort, Authentication authentication ) { - return switch (listInventoryCounts.execute(storageLocationId, status, ActorId.of(authentication.getName()))) { + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); + return switch (listInventoryCounts.execute(status, ActorId.of(authentication.getName()), pageRequest)) { case Result.Failure(var err) -> throw new InventoryCountDomainErrorException(err); - case Result.Success(var counts) -> { - var responses = counts.stream() - .map(c -> InventoryCountResponse.from(c, userLookup)) - .toList(); - yield ResponseEntity.ok(responses); - } + case Result.Success(var countPage) -> ResponseEntity.ok( + PageResponse.from(countPage, c -> InventoryCountResponse.from(c, userLookup))); }; } diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StockController.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StockController.java index 5e5f2f4..1e418d4 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StockController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StockController.java @@ -22,7 +22,9 @@ import de.effigenix.application.inventory.command.ReserveStockCommand; import de.effigenix.application.inventory.command.UnblockStockBatchCommand; import de.effigenix.application.inventory.command.UpdateStockCommand; import de.effigenix.domain.inventory.StockError; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; +import de.effigenix.shared.common.SortField; import de.effigenix.shared.security.ActorId; import de.effigenix.infrastructure.inventory.web.dto.AddStockBatchRequest; import de.effigenix.infrastructure.inventory.web.dto.BlockStockBatchRequest; @@ -33,6 +35,7 @@ import de.effigenix.infrastructure.inventory.web.dto.ReservationResponse; import de.effigenix.infrastructure.inventory.web.dto.ReserveStockRequest; import de.effigenix.infrastructure.inventory.web.dto.StockBatchResponse; import de.effigenix.infrastructure.inventory.web.dto.StockResponse; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; import de.effigenix.infrastructure.inventory.web.dto.UpdateStockRequest; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -90,18 +93,19 @@ public class StockController { @GetMapping @PreAuthorize("hasAuthority('STOCK_READ')") - public ResponseEntity> listStocks( + public ResponseEntity> listStocks( @RequestParam(required = false) String storageLocationId, - @RequestParam(required = false) String articleId + @RequestParam(required = false) String articleId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort ) { - return switch (listStocks.execute(storageLocationId, articleId)) { + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); + return switch (listStocks.execute(storageLocationId, articleId, pageRequest)) { case Result.Failure(var err) -> throw new StockDomainErrorException(err); - case Result.Success(var stocks) -> { - var responses = stocks.stream() - .map(StockResponse::from) - .toList(); - yield ResponseEntity.ok(responses); - } + case Result.Success(var stockPage) -> ResponseEntity.ok( + PageResponse.from(stockPage, StockResponse::from)); }; } diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StockMovementController.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StockMovementController.java index b77d209..4372853 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StockMovementController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StockMovementController.java @@ -7,7 +7,10 @@ import de.effigenix.application.inventory.command.RecordStockMovementCommand; import de.effigenix.domain.inventory.StockMovementError; import de.effigenix.infrastructure.inventory.web.dto.RecordStockMovementRequest; import de.effigenix.infrastructure.inventory.web.dto.StockMovementResponse; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; +import de.effigenix.shared.common.SortField; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -76,24 +79,25 @@ public class StockMovementController { @PreAuthorize("hasAuthority('STOCK_MOVEMENT_READ')") @Operation(summary = "List stock movements", description = "Filter priority (only one filter applied): stockId > articleId > batchReference > movementType > from/to") - public ResponseEntity> listMovements( + public ResponseEntity> listMovements( @RequestParam(required = false) String stockId, @RequestParam(required = false) String articleId, @RequestParam(required = false) String movementType, @RequestParam(required = false) String batchReference, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant from, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant to, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort, Authentication authentication ) { + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); return switch (listStockMovements.execute(stockId, articleId, movementType, - batchReference, from, to, ActorId.of(authentication.getName()))) { + batchReference, from, to, ActorId.of(authentication.getName()), pageRequest)) { case Result.Failure(var err) -> throw new StockMovementDomainErrorException(err); - case Result.Success(var movements) -> { - var responses = movements.stream() - .map(StockMovementResponse::from) - .toList(); - yield ResponseEntity.ok(responses); - } + case Result.Success(var movementPage) -> ResponseEntity.ok( + PageResponse.from(movementPage, StockMovementResponse::from)); }; } diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StorageLocationController.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StorageLocationController.java index c32df53..24e10b4 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StorageLocationController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StorageLocationController.java @@ -12,7 +12,10 @@ import de.effigenix.domain.inventory.StorageLocationError; import de.effigenix.infrastructure.inventory.web.dto.CreateStorageLocationRequest; import de.effigenix.infrastructure.inventory.web.dto.StorageLocationResponse; import de.effigenix.infrastructure.inventory.web.dto.UpdateStorageLocationRequest; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; +import de.effigenix.shared.common.SortField; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -60,18 +63,19 @@ public class StorageLocationController { @GetMapping @PreAuthorize("hasAuthority('STOCK_READ') or hasAuthority('STOCK_WRITE')") - public ResponseEntity> listStorageLocations( + public ResponseEntity> listStorageLocations( @RequestParam(value = "storageType", required = false) String storageType, - @RequestParam(value = "active", required = false) Boolean active + @RequestParam(value = "active", required = false) Boolean active, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort ) { - return switch (listStorageLocations.execute(storageType, active)) { + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); + return switch (listStorageLocations.execute(storageType, active, pageRequest)) { case Result.Failure(var err) -> throw new StorageLocationDomainErrorException(err); - case Result.Success(var locations) -> { - var response = locations.stream() - .map(StorageLocationResponse::from) - .toList(); - yield ResponseEntity.ok(response); - } + case Result.Success(var locationPage) -> ResponseEntity.ok( + PageResponse.from(locationPage, StorageLocationResponse::from)); }; } diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcArticleRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcArticleRepository.java index 3dd23fe..071a2e6 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcArticleRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcArticleRepository.java @@ -4,9 +4,8 @@ import de.effigenix.domain.masterdata.*; import de.effigenix.domain.masterdata.article.*; import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.domain.masterdata.supplier.SupplierId; -import de.effigenix.shared.common.Money; -import de.effigenix.shared.common.RepositoryError; -import de.effigenix.shared.common.Result; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; +import de.effigenix.shared.common.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; @@ -24,6 +23,13 @@ public class JdbcArticleRepository implements ArticleRepository { private static final Logger logger = LoggerFactory.getLogger(JdbcArticleRepository.class); + private static final Map SORT_FIELD_MAP = Map.of( + "name", "name", + "articleNumber", "article_number", + "status", "status", + "createdAt", "created_at" + ); + private final JdbcClient jdbc; public JdbcArticleRepository(JdbcClient jdbc) { @@ -47,6 +53,31 @@ public class JdbcArticleRepository implements ArticleRepository { } } + @Override + public Result> findAll(ArticleStatus status, PageRequest pageRequest) { + try { + String where = ""; + var params = new LinkedHashMap(); + if (status != null) { + where = " WHERE status = :status"; + params.put("status", status.name()); + } + long total = jdbc.sql("SELECT COUNT(*) FROM articles" + where) + .params(params).query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + if (orderBy.isEmpty()) orderBy = " ORDER BY name"; + params.put("limit", pageRequest.size()); + params.put("offset", pageRequest.offset()); + var articles = jdbc.sql("SELECT * FROM articles" + where + orderBy + " LIMIT :limit OFFSET :offset") + .params(params).query(this::mapArticleRow).list() + .stream().map(this::loadChildren).toList(); + return Result.success(Page.of(articles, pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAll(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findAll() { try { diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcCustomerRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcCustomerRepository.java index 4655ffe..e5849b4 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcCustomerRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcCustomerRepository.java @@ -3,6 +3,7 @@ package de.effigenix.infrastructure.masterdata.persistence; import de.effigenix.domain.masterdata.*; import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.customer.*; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; import de.effigenix.shared.common.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,6 +24,13 @@ public class JdbcCustomerRepository implements CustomerRepository { private static final Logger logger = LoggerFactory.getLogger(JdbcCustomerRepository.class); + private static final Map SORT_FIELD_MAP = Map.of( + "name", "name", + "type", "type", + "status", "status", + "createdAt", "created_at" + ); + private final JdbcClient jdbc; public JdbcCustomerRepository(JdbcClient jdbc) { @@ -46,6 +54,24 @@ public class JdbcCustomerRepository implements CustomerRepository { } } + @Override + public Result> findAll(PageRequest pageRequest) { + try { + long total = jdbc.sql("SELECT COUNT(*) FROM customers").query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + if (orderBy.isEmpty()) orderBy = " ORDER BY name"; + var customers = jdbc.sql("SELECT * FROM customers" + orderBy + " LIMIT :limit OFFSET :offset") + .param("limit", pageRequest.size()) + .param("offset", pageRequest.offset()) + .query(this::mapCustomerRow).list() + .stream().map(this::loadChildren).toList(); + return Result.success(Page.of(customers, pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAll(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findAll() { try { diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcProductCategoryRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcProductCategoryRepository.java index 0d6ea42..3ec5ce0 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcProductCategoryRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcProductCategoryRepository.java @@ -4,8 +4,8 @@ import de.effigenix.domain.masterdata.productcategory.CategoryName; import de.effigenix.domain.masterdata.productcategory.ProductCategory; import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.domain.masterdata.productcategory.ProductCategoryRepository; -import de.effigenix.shared.common.RepositoryError; -import de.effigenix.shared.common.Result; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; +import de.effigenix.shared.common.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; @@ -15,6 +15,7 @@ import org.springframework.stereotype.Repository; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import java.util.Map; import java.util.Optional; @Repository @@ -23,6 +24,10 @@ public class JdbcProductCategoryRepository implements ProductCategoryRepository private static final Logger logger = LoggerFactory.getLogger(JdbcProductCategoryRepository.class); + private static final Map SORT_FIELD_MAP = Map.of( + "name", "name" + ); + private final JdbcClient jdbc; public JdbcProductCategoryRepository(JdbcClient jdbc) { @@ -43,6 +48,23 @@ public class JdbcProductCategoryRepository implements ProductCategoryRepository } } + @Override + public Result> findAll(PageRequest pageRequest) { + try { + long total = jdbc.sql("SELECT COUNT(*) FROM product_categories").query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + if (orderBy.isEmpty()) orderBy = " ORDER BY name"; + var categories = jdbc.sql("SELECT * FROM product_categories" + orderBy + " LIMIT :limit OFFSET :offset") + .param("limit", pageRequest.size()) + .param("offset", pageRequest.offset()) + .query(this::mapRow).list(); + return Result.success(Page.of(categories, pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAll(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findAll() { try { diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcSupplierRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcSupplierRepository.java index 1d755ea..d7ad1d3 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcSupplierRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/JdbcSupplierRepository.java @@ -1,6 +1,7 @@ package de.effigenix.infrastructure.masterdata.persistence; import de.effigenix.domain.masterdata.supplier.*; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; import de.effigenix.shared.common.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,7 +13,9 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDate; import java.time.OffsetDateTime; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; @Repository @@ -21,6 +24,12 @@ public class JdbcSupplierRepository implements SupplierRepository { private static final Logger logger = LoggerFactory.getLogger(JdbcSupplierRepository.class); + private static final Map SORT_FIELD_MAP = Map.of( + "name", "name", + "status", "status", + "createdAt", "created_at" + ); + private final JdbcClient jdbc; public JdbcSupplierRepository(JdbcClient jdbc) { @@ -44,6 +53,31 @@ public class JdbcSupplierRepository implements SupplierRepository { } } + @Override + public Result> findAll(SupplierStatus status, PageRequest pageRequest) { + try { + String where = ""; + var params = new LinkedHashMap(); + if (status != null) { + where = " WHERE status = :status"; + params.put("status", status.name()); + } + long total = jdbc.sql("SELECT COUNT(*) FROM suppliers" + where) + .params(params).query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + if (orderBy.isEmpty()) orderBy = " ORDER BY name"; + params.put("limit", pageRequest.size()); + params.put("offset", pageRequest.offset()); + var suppliers = jdbc.sql("SELECT * FROM suppliers" + where + orderBy + " LIMIT :limit OFFSET :offset") + .params(params).query(this::mapSupplierRow).list() + .stream().map(this::loadCertificates).toList(); + return Result.success(Page.of(suppliers, pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAll(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findAll() { try { diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ArticleController.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ArticleController.java index 76c4daf..3e5f620 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ArticleController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ArticleController.java @@ -6,13 +6,14 @@ import de.effigenix.application.masterdata.article.AssignSupplier; import de.effigenix.application.masterdata.supplier.RemoveSupplier; import de.effigenix.application.masterdata.article.command.AssignSupplierCommand; import de.effigenix.application.masterdata.supplier.command.RemoveSupplierCommand; -import de.effigenix.domain.masterdata.article.Article; import de.effigenix.domain.masterdata.article.ArticleError; import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleStatus; -import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.infrastructure.masterdata.web.dto.*; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; +import de.effigenix.shared.common.SortField; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -97,29 +98,25 @@ public class ArticleController { } @GetMapping - public ResponseEntity> listArticles( - @RequestParam(value = "categoryId", required = false) String categoryId, + public ResponseEntity> listArticles( @RequestParam(value = "status", required = false) ArticleStatus status, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort, Authentication authentication ) { var actorId = extractActorId(authentication); logger.info("Listing articles by actor: {}", actorId.value()); - Result> result; - if (categoryId != null) { - result = listArticles.executeByCategory(ProductCategoryId.of(categoryId)); - } else if (status != null) { - result = listArticles.executeByStatus(status); - } else { - result = listArticles.execute(); - } + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); + var result = listArticles.execute(status, pageRequest); if (result.isFailure()) { throw new ArticleDomainErrorException(result.unsafeGetError()); } - var response = result.unsafeGetValue().stream().map(ArticleResponse::from).toList(); - return ResponseEntity.ok(response); + return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), ArticleResponse::from)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/CustomerController.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/CustomerController.java index 2344375..969da3f 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/CustomerController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/CustomerController.java @@ -2,13 +2,13 @@ package de.effigenix.infrastructure.masterdata.web.controller; import de.effigenix.application.masterdata.customer.*; import de.effigenix.application.masterdata.customer.command.*; -import de.effigenix.domain.masterdata.customer.Customer; import de.effigenix.domain.masterdata.customer.CustomerError; import de.effigenix.domain.masterdata.customer.CustomerId; -import de.effigenix.domain.masterdata.customer.CustomerStatus; -import de.effigenix.domain.masterdata.customer.CustomerType; import de.effigenix.infrastructure.masterdata.web.dto.*; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; +import de.effigenix.shared.common.SortField; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -97,29 +97,24 @@ public class CustomerController { } @GetMapping - public ResponseEntity> listCustomers( - @RequestParam(value = "type", required = false) CustomerType type, - @RequestParam(value = "status", required = false) CustomerStatus status, + public ResponseEntity> listCustomers( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort, Authentication authentication ) { var actorId = extractActorId(authentication); logger.info("Listing customers by actor: {}", actorId.value()); - Result> result; - if (type != null) { - result = listCustomers.executeByType(type); - } else if (status != null) { - result = listCustomers.executeByStatus(status); - } else { - result = listCustomers.execute(); - } + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); + var result = listCustomers.execute(pageRequest); if (result.isFailure()) { throw new CustomerDomainErrorException(result.unsafeGetError()); } - var response = result.unsafeGetValue().stream().map(CustomerResponse::from).toList(); - return ResponseEntity.ok(response); + return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), CustomerResponse::from)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ProductCategoryController.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ProductCategoryController.java index 7d344dc..d793f5c 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ProductCategoryController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ProductCategoryController.java @@ -11,6 +11,9 @@ import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.infrastructure.masterdata.web.dto.CreateProductCategoryRequest; import de.effigenix.infrastructure.masterdata.web.dto.ProductCategoryResponse; import de.effigenix.infrastructure.masterdata.web.dto.UpdateProductCategoryRequest; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; +import de.effigenix.shared.common.PageRequest; +import de.effigenix.shared.common.SortField; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -71,18 +74,24 @@ public class ProductCategoryController { } @GetMapping - public ResponseEntity> listCategories(Authentication authentication) { + public ResponseEntity> listCategories( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort, + Authentication authentication + ) { var actorId = extractActorId(authentication); logger.info("Listing product categories by actor: {}", actorId.value()); - var result = listProductCategories.execute(); + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); + var result = listProductCategories.execute(pageRequest); if (result.isFailure()) { throw new ProductCategoryDomainErrorException(result.unsafeGetError()); } - var response = result.unsafeGetValue().stream().map(ProductCategoryResponse::from).toList(); - return ResponseEntity.ok(response); + return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), ProductCategoryResponse::from)); } @PutMapping("/{id}") diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/SupplierController.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/SupplierController.java index 36d710b..766c152 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/SupplierController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/SupplierController.java @@ -2,12 +2,14 @@ package de.effigenix.infrastructure.masterdata.web.controller; import de.effigenix.application.masterdata.supplier.*; import de.effigenix.application.masterdata.supplier.command.*; -import de.effigenix.domain.masterdata.supplier.Supplier; import de.effigenix.domain.masterdata.supplier.SupplierError; import de.effigenix.domain.masterdata.supplier.SupplierId; import de.effigenix.domain.masterdata.supplier.SupplierStatus; import de.effigenix.infrastructure.masterdata.web.dto.*; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; +import de.effigenix.shared.common.SortField; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -88,26 +90,25 @@ public class SupplierController { } @GetMapping - public ResponseEntity> listSuppliers( + public ResponseEntity> listSuppliers( @RequestParam(value = "status", required = false) SupplierStatus status, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort, Authentication authentication ) { var actorId = extractActorId(authentication); logger.info("Listing suppliers by actor: {}", actorId.value()); - Result> result; - if (status != null) { - result = listSuppliers.executeByStatus(status); - } else { - result = listSuppliers.execute(); - } + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); + var result = listSuppliers.execute(status, pageRequest); if (result.isFailure()) { throw new SupplierDomainErrorException(result.unsafeGetError()); } - var response = result.unsafeGetValue().stream().map(SupplierResponse::from).toList(); - return ResponseEntity.ok(response); + return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), SupplierResponse::from)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/persistence/JdbcBatchRepository.java b/backend/src/main/java/de/effigenix/infrastructure/production/persistence/JdbcBatchRepository.java index fa60b2f..efe3ebf 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/persistence/JdbcBatchRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/persistence/JdbcBatchRepository.java @@ -2,10 +2,8 @@ package de.effigenix.infrastructure.production.persistence; import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.production.*; -import de.effigenix.shared.common.Quantity; -import de.effigenix.shared.common.RepositoryError; -import de.effigenix.shared.common.Result; -import de.effigenix.shared.common.UnitOfMeasure; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; +import de.effigenix.shared.common.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; @@ -17,10 +15,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDate; import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; +import java.util.*; @Repository @Profile("!no-db") @@ -28,6 +23,13 @@ public class JdbcBatchRepository implements BatchRepository { private static final Logger logger = LoggerFactory.getLogger(JdbcBatchRepository.class); + private static final Map SORT_FIELD_MAP = Map.of( + "batchNumber", "batch_number", + "status", "status", + "productionDate", "production_date", + "createdAt", "created_at" + ); + private final JdbcClient jdbc; public JdbcBatchRepository(JdbcClient jdbc) { @@ -138,6 +140,23 @@ public class JdbcBatchRepository implements BatchRepository { } } + @Override + public Result> findAllSummary(PageRequest pageRequest) { + try { + long total = jdbc.sql("SELECT COUNT(*) FROM batches").query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + if (orderBy.isEmpty()) orderBy = " ORDER BY created_at DESC"; + var batches = jdbc.sql("SELECT * FROM batches" + orderBy + " LIMIT :limit OFFSET :offset") + .param("limit", pageRequest.size()) + .param("offset", pageRequest.offset()) + .query(this::mapBatchRow).list(); + return Result.success(Page.of(batches, pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAllSummary(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findAllSummary() { try { diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/persistence/JdbcProductionOrderRepository.java b/backend/src/main/java/de/effigenix/infrastructure/production/persistence/JdbcProductionOrderRepository.java index 8a3092d..e807730 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/persistence/JdbcProductionOrderRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/persistence/JdbcProductionOrderRepository.java @@ -1,10 +1,8 @@ package de.effigenix.infrastructure.production.persistence; import de.effigenix.domain.production.*; -import de.effigenix.shared.common.Quantity; -import de.effigenix.shared.common.RepositoryError; -import de.effigenix.shared.common.Result; -import de.effigenix.shared.common.UnitOfMeasure; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; +import de.effigenix.shared.common.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; @@ -16,6 +14,7 @@ import java.sql.SQLException; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.List; +import java.util.Map; import java.util.Optional; @Repository @@ -24,6 +23,13 @@ public class JdbcProductionOrderRepository implements ProductionOrderRepository private static final Logger logger = LoggerFactory.getLogger(JdbcProductionOrderRepository.class); + private static final Map SORT_FIELD_MAP = Map.of( + "plannedDate", "planned_date", + "status", "status", + "priority", "priority", + "createdAt", "created_at" + ); + private final JdbcClient jdbc; public JdbcProductionOrderRepository(JdbcClient jdbc) { @@ -44,6 +50,23 @@ public class JdbcProductionOrderRepository implements ProductionOrderRepository } } + @Override + public Result> findAll(PageRequest pageRequest) { + try { + long total = jdbc.sql("SELECT COUNT(*) FROM production_orders").query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + if (orderBy.isEmpty()) orderBy = " ORDER BY planned_date, created_at DESC"; + var orders = jdbc.sql("SELECT * FROM production_orders" + orderBy + " LIMIT :limit OFFSET :offset") + .param("limit", pageRequest.size()) + .param("offset", pageRequest.offset()) + .query(this::mapRow).list(); + return Result.success(Page.of(orders, pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAll(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findAll() { try { diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/persistence/JdbcRecipeRepository.java b/backend/src/main/java/de/effigenix/infrastructure/production/persistence/JdbcRecipeRepository.java index 09d1f37..d4a186a 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/persistence/JdbcRecipeRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/persistence/JdbcRecipeRepository.java @@ -1,10 +1,8 @@ package de.effigenix.infrastructure.production.persistence; import de.effigenix.domain.production.*; -import de.effigenix.shared.common.Quantity; -import de.effigenix.shared.common.RepositoryError; -import de.effigenix.shared.common.Result; -import de.effigenix.shared.common.UnitOfMeasure; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; +import de.effigenix.shared.common.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; @@ -15,6 +13,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.time.OffsetDateTime; import java.util.List; +import java.util.Map; import java.util.Optional; @Repository @@ -23,6 +22,13 @@ public class JdbcRecipeRepository implements RecipeRepository { private static final Logger logger = LoggerFactory.getLogger(JdbcRecipeRepository.class); + private static final Map SORT_FIELD_MAP = Map.of( + "name", "name", + "version", "version", + "status", "status", + "createdAt", "created_at" + ); + private final JdbcClient jdbc; public JdbcRecipeRepository(JdbcClient jdbc) { @@ -47,6 +53,24 @@ public class JdbcRecipeRepository implements RecipeRepository { } } + @Override + public Result> findAll(PageRequest pageRequest) { + try { + long total = jdbc.sql("SELECT COUNT(*) FROM recipes").query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + if (orderBy.isEmpty()) orderBy = " ORDER BY name, version"; + var recipes = jdbc.sql("SELECT * FROM recipes" + orderBy + " LIMIT :limit OFFSET :offset") + .param("limit", pageRequest.size()) + .param("offset", pageRequest.offset()) + .query(this::mapRecipeRow).list() + .stream().map(this::loadChildren).toList(); + return Result.success(Page.of(recipes, pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAll(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findAll() { try { diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/BatchController.java b/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/BatchController.java index ea7c8c9..7827f5b 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/BatchController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/BatchController.java @@ -30,6 +30,10 @@ import de.effigenix.infrastructure.production.web.dto.CancelBatchRequest; import de.effigenix.infrastructure.production.web.dto.CompleteBatchRequest; import de.effigenix.infrastructure.production.web.dto.PlanBatchRequest; import de.effigenix.infrastructure.production.web.dto.RecordConsumptionRequest; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; +import de.effigenix.shared.common.SortField; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -100,17 +104,34 @@ public class BatchController { @GetMapping @PreAuthorize("hasAuthority('BATCH_READ')") - public ResponseEntity> listBatches( + public ResponseEntity> listBatches( @RequestParam(value = "status", required = false) String status, @RequestParam(value = "productionDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate productionDate, @RequestParam(value = "articleId", required = false) String articleId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort, Authentication authentication ) { var actorId = ActorId.of(authentication.getName()); + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); - var result = switch (filterType(status, productionDate, articleId)) { - case "ambiguous" -> throw new BatchDomainErrorException( + var filterKey = filterType(status, productionDate, articleId); + if ("ambiguous".equals(filterKey)) { + throw new BatchDomainErrorException( new BatchError.ValidationFailure("Only one filter allowed at a time: status, productionDate, or articleId")); + } + + if ("none".equals(filterKey)) { + var result = listBatches.execute(actorId, pageRequest); + if (result.isFailure()) { + throw new BatchDomainErrorException(result.unsafeGetError()); + } + return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), BatchSummaryResponse::from)); + } + + var result = switch (filterKey) { case "status" -> { try { yield listBatches.executeByStatus(BatchStatus.valueOf(status), actorId); @@ -121,17 +142,16 @@ public class BatchController { } case "productionDate" -> listBatches.executeByProductionDate(productionDate, actorId); case "articleId" -> listBatches.executeByArticleId(articleId, actorId); - default -> listBatches.execute(actorId); + default -> throw new IllegalStateException("Unexpected filter: " + filterKey); }; if (result.isFailure()) { throw new BatchDomainErrorException(result.unsafeGetError()); } - var summaries = result.unsafeGetValue().stream() - .map(BatchSummaryResponse::from) - .toList(); - return ResponseEntity.ok(summaries); + var list = result.unsafeGetValue(); + var batchPage = Page.of(list, 0, list.size() > 0 ? list.size() : 20, list.size()); + return ResponseEntity.ok(PageResponse.from(batchPage, BatchSummaryResponse::from)); } @GetMapping("/by-number/{batchNumber}") diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/ProductionOrderController.java b/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/ProductionOrderController.java index 94ffaa5..4381a09 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/ProductionOrderController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/ProductionOrderController.java @@ -22,7 +22,10 @@ import de.effigenix.infrastructure.production.web.dto.CancelProductionOrderReque import de.effigenix.infrastructure.production.web.dto.CreateProductionOrderRequest; import de.effigenix.infrastructure.production.web.dto.ProductionOrderResponse; import de.effigenix.infrastructure.production.web.dto.RescheduleProductionOrderRequest; - +import de.effigenix.infrastructure.shared.web.dto.PageResponse; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; +import de.effigenix.shared.common.SortField; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -79,15 +82,20 @@ public class ProductionOrderController { @GetMapping @PreAuthorize("hasAuthority('PRODUCTION_ORDER_READ')") - public ResponseEntity> listProductionOrders( + public ResponseEntity> listProductionOrders( @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate dateFrom, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate dateTo, @RequestParam(required = false) ProductionOrderStatus status, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort, Authentication authentication ) { logger.info("Listing production orders by actor: {}", authentication.getName()); var actor = ActorId.of(authentication.getName()); + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); boolean hasDateRange = dateFrom != null && dateTo != null; boolean hasPartialDate = (dateFrom != null) != (dateTo != null); @@ -96,22 +104,31 @@ public class ProductionOrderController { return ResponseEntity.badRequest().build(); } + boolean hasFilter = hasDateRange || status != null; + + if (!hasFilter) { + var result = listProductionOrders.execute(actor, pageRequest); + if (result.isFailure()) { + throw new ProductionOrderDomainErrorException(result.unsafeGetError()); + } + return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), + order -> ProductionOrderResponse.from(order, resolveBatchNumber(order)))); + } + var result = hasDateRange && status != null ? listProductionOrders.executeByDateRangeAndStatus(dateFrom, dateTo, status, actor) : hasDateRange ? listProductionOrders.executeByDateRange(dateFrom, dateTo, actor) - : status != null - ? listProductionOrders.executeByStatus(status, actor) - : listProductionOrders.execute(actor); + : listProductionOrders.executeByStatus(status, actor); if (result.isFailure()) { throw new ProductionOrderDomainErrorException(result.unsafeGetError()); } - var responses = result.unsafeGetValue().stream() - .map(order -> ProductionOrderResponse.from(order, resolveBatchNumber(order))) - .toList(); - return ResponseEntity.ok(responses); + var list = result.unsafeGetValue(); + var orderPage = Page.of(list, 0, list.size() > 0 ? list.size() : 20, list.size()); + return ResponseEntity.ok(PageResponse.from(orderPage, + order -> ProductionOrderResponse.from(order, resolveBatchNumber(order)))); } @GetMapping("/{id}") diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/RecipeController.java b/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/RecipeController.java index e074873..3b62f1c 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/RecipeController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/RecipeController.java @@ -24,6 +24,10 @@ import de.effigenix.infrastructure.production.web.dto.AddRecipeIngredientRequest import de.effigenix.infrastructure.production.web.dto.CreateRecipeRequest; import de.effigenix.infrastructure.production.web.dto.RecipeResponse; import de.effigenix.infrastructure.production.web.dto.RecipeSummaryResponse; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; +import de.effigenix.shared.common.SortField; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -94,11 +98,16 @@ public class RecipeController { @GetMapping @PreAuthorize("hasAuthority('RECIPE_READ')") - public ResponseEntity> listRecipes( + public ResponseEntity> listRecipes( @RequestParam(value = "status", required = false) String status, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort, Authentication authentication ) { var actorId = extractActorId(authentication); + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); RecipeStatus parsedStatus = null; if (status != null) { @@ -110,18 +119,22 @@ public class RecipeController { } } - var result = (parsedStatus != null) - ? listRecipes.executeByStatus(parsedStatus, actorId) - : listRecipes.execute(actorId); + if (parsedStatus != null) { + var result = listRecipes.executeByStatus(parsedStatus, actorId); + if (result.isFailure()) { + throw new RecipeDomainErrorException(result.unsafeGetError()); + } + var list = result.unsafeGetValue(); + var recipePage = Page.of(list, 0, list.size() > 0 ? list.size() : 20, list.size()); + return ResponseEntity.ok(PageResponse.from(recipePage, RecipeSummaryResponse::from)); + } + var result = listRecipes.execute(actorId, pageRequest); if (result.isFailure()) { throw new RecipeDomainErrorException(result.unsafeGetError()); } - var summaries = result.unsafeGetValue().stream() - .map(RecipeSummaryResponse::from) - .toList(); - return ResponseEntity.ok(summaries); + return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), RecipeSummaryResponse::from)); } @PostMapping diff --git a/backend/src/main/java/de/effigenix/infrastructure/shared/InMemoryCountryRepository.java b/backend/src/main/java/de/effigenix/infrastructure/shared/InMemoryCountryRepository.java index d957900..7b5ae5d 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/shared/InMemoryCountryRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/shared/InMemoryCountryRepository.java @@ -2,6 +2,8 @@ package de.effigenix.infrastructure.shared; import de.effigenix.shared.common.Country; import de.effigenix.shared.common.CountryRepository; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import java.util.*; import java.util.stream.Collectors; @@ -286,6 +288,15 @@ public class InMemoryCountryRepository implements CountryRepository { return allSorted; } + @Override + public Page findAll(PageRequest pageRequest) { + int total = allSorted.size(); + int fromIndex = Math.min(pageRequest.offset(), total); + int toIndex = Math.min(fromIndex + pageRequest.size(), total); + var content = allSorted.subList(fromIndex, toIndex); + return Page.of(content, pageRequest.page(), pageRequest.size(), total); + } + @Override public List search(String query) { if (query == null || query.isBlank()) { diff --git a/backend/src/main/java/de/effigenix/infrastructure/shared/persistence/PaginationHelper.java b/backend/src/main/java/de/effigenix/infrastructure/shared/persistence/PaginationHelper.java new file mode 100644 index 0000000..53a39b6 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/shared/persistence/PaginationHelper.java @@ -0,0 +1,23 @@ +package de.effigenix.infrastructure.shared.persistence; + +import de.effigenix.shared.common.SortField; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public final class PaginationHelper { + + private PaginationHelper() {} + + public static String buildOrderByClause(List sort, Map allowedFields) { + if (sort == null || sort.isEmpty()) { + return ""; + } + var clauses = sort.stream() + .filter(sf -> allowedFields.containsKey(sf.field())) + .map(sf -> allowedFields.get(sf.field()) + " " + sf.direction().name()) + .collect(Collectors.joining(", ")); + return clauses.isEmpty() ? "" : " ORDER BY " + clauses; + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/shared/web/CountryController.java b/backend/src/main/java/de/effigenix/infrastructure/shared/web/CountryController.java index 35cc84b..014f9d5 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/shared/web/CountryController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/shared/web/CountryController.java @@ -1,6 +1,9 @@ package de.effigenix.infrastructure.shared.web; import de.effigenix.application.shared.ListCountries; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; +import de.effigenix.shared.common.PageRequest; +import de.effigenix.shared.common.SortField; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; @@ -26,9 +29,21 @@ public class CountryController { public record CountryResponse(String code, String name) {} @GetMapping + public ResponseEntity> list( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort) { + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); + var countryPage = listCountries.execute(pageRequest); + return ResponseEntity.ok(PageResponse.from(countryPage, + c -> new CountryResponse(c.code(), c.name()))); + } + + @GetMapping(params = "q") public ResponseEntity> search( - @RequestParam(name = "q", required = false, defaultValue = "") String query) { - var countries = listCountries.execute(query); + @RequestParam(name = "q") String query) { + var countries = listCountries.search(query); var response = countries.stream() .map(c -> new CountryResponse(c.code(), c.name())) .toList(); diff --git a/backend/src/main/java/de/effigenix/infrastructure/shared/web/dto/PageResponse.java b/backend/src/main/java/de/effigenix/infrastructure/shared/web/dto/PageResponse.java new file mode 100644 index 0000000..c60e213 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/shared/web/dto/PageResponse.java @@ -0,0 +1,26 @@ +package de.effigenix.infrastructure.shared.web.dto; + +import de.effigenix.shared.common.Page; + +import java.util.List; +import java.util.function.Function; + +public record PageResponse( + List content, + PageInfo page +) { + public record PageInfo(int number, int size, long totalElements, int totalPages) {} + + public static PageResponse from(Page domainPage) { + return new PageResponse<>(domainPage.content(), + new PageInfo(domainPage.number(), domainPage.size(), + domainPage.totalElements(), domainPage.totalPages())); + } + + public static PageResponse from(Page domainPage, Function mapper) { + var content = domainPage.content().stream().map(mapper).toList(); + return new PageResponse<>(content, + new PageInfo(domainPage.number(), domainPage.size(), + domainPage.totalElements(), domainPage.totalPages())); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubArticleRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubArticleRepository.java index 31f1cf3..86ac1aa 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubArticleRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubArticleRepository.java @@ -6,6 +6,8 @@ import de.effigenix.domain.masterdata.article.ArticleNumber; import de.effigenix.domain.masterdata.article.ArticleRepository; import de.effigenix.domain.masterdata.article.ArticleStatus; import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -26,6 +28,11 @@ public class StubArticleRepository implements ArticleRepository { return Result.failure(STUB_ERROR); } + @Override + public Result> findAll(ArticleStatus status, PageRequest pageRequest) { + return Result.success(Page.empty(pageRequest.size())); + } + @Override public Result> findAll() { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubBatchRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubBatchRepository.java index 2636408..265ec3a 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubBatchRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubBatchRepository.java @@ -7,6 +7,8 @@ import de.effigenix.domain.production.BatchRepository; import de.effigenix.domain.production.BatchStatus; import de.effigenix.domain.production.RecipeId; import de.effigenix.domain.production.TracedBatch; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -53,6 +55,11 @@ public class StubBatchRepository implements BatchRepository { return Result.failure(STUB_ERROR); } + @Override + public Result> findAllSummary(PageRequest pageRequest) { + return Result.success(Page.empty(pageRequest.size())); + } + @Override public Result> findAllSummary() { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubCustomerRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubCustomerRepository.java index 91f07ee..3668e7d 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubCustomerRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubCustomerRepository.java @@ -6,6 +6,8 @@ import de.effigenix.domain.masterdata.customer.CustomerName; import de.effigenix.domain.masterdata.customer.CustomerRepository; import de.effigenix.domain.masterdata.customer.CustomerStatus; import de.effigenix.domain.masterdata.customer.CustomerType; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -26,6 +28,11 @@ public class StubCustomerRepository implements CustomerRepository { return Result.failure(STUB_ERROR); } + @Override + public Result> findAll(PageRequest pageRequest) { + return Result.success(Page.empty(pageRequest.size())); + } + @Override public Result> findAll() { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubInventoryCountRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubInventoryCountRepository.java index a6a13bb..eb71fab 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubInventoryCountRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubInventoryCountRepository.java @@ -5,6 +5,8 @@ import de.effigenix.domain.inventory.InventoryCountId; import de.effigenix.domain.inventory.InventoryCountRepository; import de.effigenix.domain.inventory.InventoryCountStatus; import de.effigenix.domain.inventory.StorageLocationId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -25,6 +27,11 @@ public class StubInventoryCountRepository implements InventoryCountRepository { return Result.failure(STUB_ERROR); } + @Override + public Result> findAll(InventoryCountStatus status, PageRequest pageRequest) { + return Result.failure(STUB_ERROR); + } + @Override public Result> findAll() { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubProductCategoryRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubProductCategoryRepository.java index cf7e5f0..468e6f7 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubProductCategoryRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubProductCategoryRepository.java @@ -4,6 +4,8 @@ import de.effigenix.domain.masterdata.productcategory.CategoryName; import de.effigenix.domain.masterdata.productcategory.ProductCategory; import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.domain.masterdata.productcategory.ProductCategoryRepository; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -24,6 +26,11 @@ public class StubProductCategoryRepository implements ProductCategoryRepository return Result.failure(STUB_ERROR); } + @Override + public Result> findAll(PageRequest pageRequest) { + return Result.success(Page.empty(pageRequest.size())); + } + @Override public Result> findAll() { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubProductionOrderRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubProductionOrderRepository.java index a413972..61363ae 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubProductionOrderRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubProductionOrderRepository.java @@ -4,6 +4,8 @@ import de.effigenix.domain.production.ProductionOrder; import de.effigenix.domain.production.ProductionOrderId; import de.effigenix.domain.production.ProductionOrderRepository; import de.effigenix.domain.production.ProductionOrderStatus; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -25,6 +27,11 @@ public class StubProductionOrderRepository implements ProductionOrderRepository return Result.failure(STUB_ERROR); } + @Override + public Result> findAll(PageRequest pageRequest) { + return Result.success(Page.empty(pageRequest.size())); + } + @Override public Result> findAll() { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubRecipeRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubRecipeRepository.java index 71fdc4b..3be5288 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubRecipeRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubRecipeRepository.java @@ -4,6 +4,8 @@ import de.effigenix.domain.production.Recipe; import de.effigenix.domain.production.RecipeId; import de.effigenix.domain.production.RecipeRepository; import de.effigenix.domain.production.RecipeStatus; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -24,6 +26,11 @@ public class StubRecipeRepository implements RecipeRepository { return Result.failure(STUB_ERROR); } + @Override + public Result> findAll(PageRequest pageRequest) { + return Result.success(Page.empty(pageRequest.size())); + } + @Override public Result> findAll() { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubRoleRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubRoleRepository.java index 22b02e6..3acdafa 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubRoleRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubRoleRepository.java @@ -4,6 +4,8 @@ import de.effigenix.domain.usermanagement.Role; import de.effigenix.domain.usermanagement.RoleId; import de.effigenix.domain.usermanagement.RoleName; import de.effigenix.domain.usermanagement.RoleRepository; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -29,6 +31,11 @@ public class StubRoleRepository implements RoleRepository { return Result.failure(STUB_ERROR); } + @Override + public Result> findAll(PageRequest pageRequest) { + return Result.success(Page.empty(pageRequest.size())); + } + @Override public Result> findAll() { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubStockMovementRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubStockMovementRepository.java index f495ae7..4022b95 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubStockMovementRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubStockMovementRepository.java @@ -6,6 +6,8 @@ import de.effigenix.domain.inventory.StockMovement; import de.effigenix.domain.inventory.StockMovementId; import de.effigenix.domain.inventory.StockMovementRepository; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -27,6 +29,14 @@ public class StubStockMovementRepository implements StockMovementRepository { return Result.failure(STUB_ERROR); } + @Override + public Result> findAll(StockId stockId, ArticleId articleId, + MovementType movementType, String batchRef, + Instant from, Instant to, + PageRequest pageRequest) { + return Result.failure(STUB_ERROR); + } + @Override public Result> findAll() { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubStockRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubStockRepository.java index 3130ca5..eab8b1b 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubStockRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubStockRepository.java @@ -5,6 +5,8 @@ import de.effigenix.domain.inventory.StockId; import de.effigenix.domain.inventory.StockRepository; import de.effigenix.domain.inventory.StorageLocationId; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -26,6 +28,21 @@ public class StubStockRepository implements StockRepository { return Result.failure(STUB_ERROR); } + @Override + public Result> findAll(PageRequest pageRequest) { + return Result.failure(STUB_ERROR); + } + + @Override + public Result> findAllByStorageLocationId(StorageLocationId storageLocationId, PageRequest pageRequest) { + return Result.failure(STUB_ERROR); + } + + @Override + public Result> findAllByArticleId(ArticleId articleId, PageRequest pageRequest) { + return Result.failure(STUB_ERROR); + } + @Override public Result> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.failure(STUB_ERROR); @@ -61,6 +78,11 @@ public class StubStockRepository implements StockRepository { return Result.failure(STUB_ERROR); } + @Override + public Result> findAllByBatchId(String batchId) { + return Result.failure(STUB_ERROR); + } + @Override public Result save(Stock stock) { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubStorageLocationRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubStorageLocationRepository.java index 4f6c508..9ec2035 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubStorageLocationRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubStorageLocationRepository.java @@ -5,6 +5,8 @@ import de.effigenix.domain.inventory.StorageLocationId; import de.effigenix.domain.inventory.StorageLocationName; import de.effigenix.domain.inventory.StorageLocationRepository; import de.effigenix.domain.inventory.StorageType; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -25,6 +27,11 @@ public class StubStorageLocationRepository implements StorageLocationRepository return Result.failure(STUB_ERROR); } + @Override + public Result> findAll(StorageType storageType, Boolean active, PageRequest pageRequest) { + return Result.failure(STUB_ERROR); + } + @Override public Result> findAll() { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubSupplierRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubSupplierRepository.java index b6ebe50..290b5d0 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubSupplierRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubSupplierRepository.java @@ -5,6 +5,8 @@ import de.effigenix.domain.masterdata.supplier.SupplierId; import de.effigenix.domain.masterdata.supplier.SupplierName; import de.effigenix.domain.masterdata.supplier.SupplierRepository; import de.effigenix.domain.masterdata.supplier.SupplierStatus; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -25,6 +27,11 @@ public class StubSupplierRepository implements SupplierRepository { return Result.failure(STUB_ERROR); } + @Override + public Result> findAll(SupplierStatus status, PageRequest pageRequest) { + return Result.success(Page.empty(pageRequest.size())); + } + @Override public Result> findAll() { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/stub/StubUserRepository.java b/backend/src/main/java/de/effigenix/infrastructure/stub/StubUserRepository.java index 72485d0..1ab5209 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/stub/StubUserRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/stub/StubUserRepository.java @@ -4,6 +4,8 @@ import de.effigenix.domain.usermanagement.User; import de.effigenix.domain.usermanagement.UserId; import de.effigenix.domain.usermanagement.UserRepository; import de.effigenix.domain.usermanagement.UserStatus; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; @@ -44,6 +46,11 @@ public class StubUserRepository implements UserRepository { return Result.failure(STUB_ERROR); } + @Override + public Result> findAll(String branchId, PageRequest pageRequest) { + return Result.success(Page.empty(pageRequest.size())); + } + @Override public Result> findAll() { return Result.failure(STUB_ERROR); diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/JdbcRoleRepository.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/JdbcRoleRepository.java index 79f568a..58984a8 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/JdbcRoleRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/JdbcRoleRepository.java @@ -1,8 +1,8 @@ package de.effigenix.infrastructure.usermanagement.persistence; import de.effigenix.domain.usermanagement.*; -import de.effigenix.shared.common.RepositoryError; -import de.effigenix.shared.common.Result; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; +import de.effigenix.shared.common.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; @@ -19,6 +19,10 @@ public class JdbcRoleRepository implements RoleRepository { private static final Logger logger = LoggerFactory.getLogger(JdbcRoleRepository.class); + private static final Map SORT_FIELD_MAP = Map.of( + "name", "name" + ); + private final JdbcClient jdbc; public JdbcRoleRepository(JdbcClient jdbc) { @@ -59,6 +63,24 @@ public class JdbcRoleRepository implements RoleRepository { } } + @Override + public Result> findAll(PageRequest pageRequest) { + try { + long total = jdbc.sql("SELECT COUNT(*) FROM roles").query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + if (orderBy.isEmpty()) orderBy = " ORDER BY name"; + var roles = jdbc.sql("SELECT * FROM roles" + orderBy + " LIMIT :limit OFFSET :offset") + .param("limit", pageRequest.size()) + .param("offset", pageRequest.offset()) + .query(this::mapRoleRow).list() + .stream().map(this::loadPermissions).toList(); + return Result.success(Page.of(roles, pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAll(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findAll() { try { diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/JdbcUserRepository.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/JdbcUserRepository.java index 734b444..31ad3f5 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/JdbcUserRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/JdbcUserRepository.java @@ -1,8 +1,8 @@ package de.effigenix.infrastructure.usermanagement.persistence; import de.effigenix.domain.usermanagement.*; -import de.effigenix.shared.common.RepositoryError; -import de.effigenix.shared.common.Result; +import de.effigenix.infrastructure.shared.persistence.PaginationHelper; +import de.effigenix.shared.common.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; @@ -20,6 +20,13 @@ public class JdbcUserRepository implements UserRepository { private static final Logger logger = LoggerFactory.getLogger(JdbcUserRepository.class); + private static final Map SORT_FIELD_MAP = Map.of( + "username", "username", + "email", "email", + "status", "status", + "createdAt", "created_at" + ); + private final JdbcClient jdbc; private final RoleRepository roleRepository; @@ -113,6 +120,31 @@ public class JdbcUserRepository implements UserRepository { } } + @Override + public Result> findAll(String branchId, PageRequest pageRequest) { + try { + String where = ""; + var params = new LinkedHashMap(); + if (branchId != null) { + where = " WHERE branch_id = :branchId"; + params.put("branchId", branchId); + } + long total = jdbc.sql("SELECT COUNT(*) FROM users" + where) + .params(params).query(Long.class).single(); + String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP); + if (orderBy.isEmpty()) orderBy = " ORDER BY username"; + params.put("limit", pageRequest.size()); + params.put("offset", pageRequest.offset()); + var users = jdbc.sql("SELECT * FROM users" + where + orderBy + " LIMIT :limit OFFSET :offset") + .params(params).query(this::mapUserRow).list() + .stream().map(this::loadRoles).toList(); + return Result.success(Page.of(users, pageRequest.page(), pageRequest.size(), total)); + } catch (Exception e) { + logger.trace("Database error in findAll(paginated)", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override public Result> findAll() { try { diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/RoleController.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/RoleController.java index 11aea38..52fbed4 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/RoleController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/RoleController.java @@ -1,9 +1,11 @@ package de.effigenix.infrastructure.usermanagement.web.controller; import de.effigenix.application.usermanagement.dto.RoleDTO; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; -import de.effigenix.domain.usermanagement.Role; +import de.effigenix.shared.common.SortField; import de.effigenix.domain.usermanagement.RoleRepository; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.Operation; @@ -20,10 +22,10 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; -import java.util.stream.Collectors; /** * REST Controller for Role Management endpoints. @@ -99,20 +101,24 @@ public class RoleController { @ApiResponse(responseCode = "403", description = "Missing USER_MANAGEMENT permission"), @ApiResponse(responseCode = "401", description = "Authentication required") }) - public ResponseEntity> listRoles(Authentication authentication) { + public ResponseEntity> listRoles( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort, + Authentication authentication + ) { ActorId actorId = extractActorId(authentication); logger.info("Listing roles by actor: {}", actorId.value()); - var result = roleRepository.findAll(); + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); + var result = roleRepository.findAll(pageRequest); if (result.isFailure()) { throw new RoleDomainErrorException(result.unsafeGetError()); } - List roles = result.unsafeGetValue().stream() - .map(RoleDTO::from) - .collect(Collectors.toList()); - logger.info("Found {} roles", roles.size()); - return ResponseEntity.ok(roles); + logger.info("Found {} roles", result.unsafeGetValue().totalElements()); + return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), RoleDTO::from)); } // ==================== Helper Methods ==================== diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/UserController.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/UserController.java index d58bbf5..8e5f920 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/UserController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/UserController.java @@ -5,8 +5,11 @@ import de.effigenix.application.usermanagement.command.*; import de.effigenix.application.usermanagement.dto.UserDTO; import de.effigenix.domain.usermanagement.RoleName; import de.effigenix.domain.usermanagement.UserError; +import de.effigenix.infrastructure.shared.web.dto.PageResponse; import de.effigenix.infrastructure.usermanagement.web.dto.*; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; +import de.effigenix.shared.common.SortField; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -111,14 +114,22 @@ public class UserController { @ApiResponse(responseCode = "200", description = "Users retrieved successfully"), @ApiResponse(responseCode = "401", description = "Authentication required") }) - public ResponseEntity> listUsers(Authentication authentication) { + public ResponseEntity> listUsers( + @RequestParam(value = "branchId", required = false) String branchId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) List sort, + Authentication authentication + ) { ActorId actorId = extractActorId(authentication); logger.info("Listing users by actor: {}", actorId.value()); - Result> result = listUsers.execute(actorId); + var pageRequest = PageRequest.of(page, Math.min(size, 100), + sort != null ? sort.stream().map(SortField::parse).toList() : List.of()); + Result> result = listUsers.execute(branchId, actorId, pageRequest); if (result.isFailure()) throw new DomainErrorException(result.unsafeGetError()); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue())); } @GetMapping("/{id}") diff --git a/backend/src/main/java/de/effigenix/shared/common/CountryRepository.java b/backend/src/main/java/de/effigenix/shared/common/CountryRepository.java index c85196b..7ba2bf5 100644 --- a/backend/src/main/java/de/effigenix/shared/common/CountryRepository.java +++ b/backend/src/main/java/de/effigenix/shared/common/CountryRepository.java @@ -5,6 +5,7 @@ import java.util.Optional; public interface CountryRepository { List findAll(); + Page findAll(PageRequest pageRequest); List search(String query); Optional findByCode(String code); } diff --git a/backend/src/main/java/de/effigenix/shared/common/Page.java b/backend/src/main/java/de/effigenix/shared/common/Page.java new file mode 100644 index 0000000..f944da2 --- /dev/null +++ b/backend/src/main/java/de/effigenix/shared/common/Page.java @@ -0,0 +1,25 @@ +package de.effigenix.shared.common; + +import java.util.List; +import java.util.function.Function; + +public record Page(List content, int number, int size, long totalElements, int totalPages) { + + public Page { + content = content != null ? List.copyOf(content) : List.of(); + } + + public static Page of(List content, int number, int size, long totalElements) { + int totalPages = size > 0 ? (int) Math.ceil((double) totalElements / size) : 0; + return new Page<>(content, number, size, totalElements, totalPages); + } + + public static Page empty(int size) { + return new Page<>(List.of(), 0, size, 0, 0); + } + + public Page map(Function mapper) { + var mapped = content.stream().map(mapper).toList(); + return new Page<>(mapped, number, size, totalElements, totalPages); + } +} diff --git a/backend/src/main/java/de/effigenix/shared/common/PageRequest.java b/backend/src/main/java/de/effigenix/shared/common/PageRequest.java new file mode 100644 index 0000000..b875fc6 --- /dev/null +++ b/backend/src/main/java/de/effigenix/shared/common/PageRequest.java @@ -0,0 +1,30 @@ +package de.effigenix.shared.common; + +import java.util.List; + +public record PageRequest(int page, int size, List sort) { + + private static final int MAX_SIZE = 100; + + public PageRequest { + if (page < 0) { + throw new IllegalArgumentException("Page must be >= 0, got: " + page); + } + if (size < 1 || size > MAX_SIZE) { + throw new IllegalArgumentException("Size must be between 1 and " + MAX_SIZE + ", got: " + size); + } + sort = sort != null ? List.copyOf(sort) : List.of(); + } + + public static PageRequest of(int page, int size) { + return new PageRequest(page, size, List.of()); + } + + public static PageRequest of(int page, int size, List sort) { + return new PageRequest(page, size, sort); + } + + public int offset() { + return page * size; + } +} diff --git a/backend/src/main/java/de/effigenix/shared/common/SortDirection.java b/backend/src/main/java/de/effigenix/shared/common/SortDirection.java new file mode 100644 index 0000000..3ee2286 --- /dev/null +++ b/backend/src/main/java/de/effigenix/shared/common/SortDirection.java @@ -0,0 +1,5 @@ +package de.effigenix.shared.common; + +public enum SortDirection { + ASC, DESC +} diff --git a/backend/src/main/java/de/effigenix/shared/common/SortField.java b/backend/src/main/java/de/effigenix/shared/common/SortField.java new file mode 100644 index 0000000..7f01969 --- /dev/null +++ b/backend/src/main/java/de/effigenix/shared/common/SortField.java @@ -0,0 +1,31 @@ +package de.effigenix.shared.common; + +public record SortField(String field, SortDirection direction) { + + public static SortField asc(String field) { + return new SortField(field, SortDirection.ASC); + } + + public static SortField desc(String field) { + return new SortField(field, SortDirection.DESC); + } + + public static SortField parse(String value) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("Sort field must not be blank"); + } + var parts = value.split(",", 2); + var field = parts[0].trim(); + if (field.isEmpty()) { + throw new IllegalArgumentException("Sort field name must not be empty"); + } + var direction = SortDirection.ASC; + if (parts.length == 2) { + var dirStr = parts[1].trim().toUpperCase(); + if ("DESC".equals(dirStr)) { + direction = SortDirection.DESC; + } + } + return new SortField(field, direction); + } +} diff --git a/backend/src/test/java/de/effigenix/application/inventory/CheckStockExpiryTest.java b/backend/src/test/java/de/effigenix/application/inventory/CheckStockExpiryTest.java index 5f89a33..745b007 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/CheckStockExpiryTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/CheckStockExpiryTest.java @@ -2,6 +2,8 @@ package de.effigenix.application.inventory; import de.effigenix.domain.inventory.*; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -261,10 +263,14 @@ class CheckStockExpiryTest { // Unused methods for this test @Override public Result> findAllBelowMinimumLevel() { return Result.success(List.of()); } @Override public Result> findById(StockId id) { return Result.success(Optional.empty()); } + @Override public Result> findAll(PageRequest pageRequest) { return Result.success(Page.empty(pageRequest.size())); } + @Override public Result> findAllByStorageLocationId(StorageLocationId storageLocationId, PageRequest pageRequest) { return Result.success(Page.empty(pageRequest.size())); } + @Override public Result> findAllByArticleId(ArticleId articleId, PageRequest pageRequest) { return Result.success(Page.empty(pageRequest.size())); } @Override public Result> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(Optional.empty()); } @Override public Result existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(false); } @Override public Result> findAll() { return Result.success(List.of()); } @Override public Result> findAllByStorageLocationId(StorageLocationId storageLocationId) { return Result.success(List.of()); } @Override public Result> findAllByArticleId(ArticleId articleId) { return Result.success(List.of()); } + @Override public Result> findAllByBatchId(String batchId) { return Result.success(List.of()); } } } diff --git a/backend/src/test/java/de/effigenix/application/inventory/ListInventoryCountsTest.java b/backend/src/test/java/de/effigenix/application/inventory/ListInventoryCountsTest.java index 78a8d69..89f5a4a 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/ListInventoryCountsTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/ListInventoryCountsTest.java @@ -1,6 +1,8 @@ package de.effigenix.application.inventory; import de.effigenix.domain.inventory.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; @@ -18,6 +20,8 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -31,6 +35,7 @@ class ListInventoryCountsTest { private InventoryCount count1; private InventoryCount count2; private final ActorId actorId = ActorId.of("user-1"); + private final PageRequest pageRequest = PageRequest.of(0, 100); @BeforeEach void setUp() { @@ -65,105 +70,55 @@ class ListInventoryCountsTest { @Test @DisplayName("should return all counts when no filter provided") void shouldReturnAllCountsWhenNoFilter() { - when(inventoryCountRepository.findAll()).thenReturn(Result.success(List.of(count1, count2))); + when(inventoryCountRepository.findAll(isNull(), eq(pageRequest))) + .thenReturn(Result.success(Page.of(List.of(count1, count2), 0, 100, 2))); - var result = listInventoryCounts.execute(null, null, actorId); + var result = listInventoryCounts.execute(null, actorId, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); - verify(inventoryCountRepository).findAll(); - } - - @Test - @DisplayName("should filter by storageLocationId") - void shouldFilterByStorageLocationId() { - when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1"))) - .thenReturn(Result.success(List.of(count1))); - - var result = listInventoryCounts.execute("location-1", null, actorId); - - assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - verify(inventoryCountRepository).findByStorageLocationId(StorageLocationId.of("location-1")); - verify(inventoryCountRepository, never()).findAll(); + assertThat(result.unsafeGetValue().content()).hasSize(2); } @Test @DisplayName("should fail with RepositoryFailure when repository fails for findAll") void shouldFailWhenRepositoryFailsForFindAll() { - when(inventoryCountRepository.findAll()) + when(inventoryCountRepository.findAll(isNull(), eq(pageRequest))) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = listInventoryCounts.execute(null, null, actorId); + var result = listInventoryCounts.execute(null, actorId, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class); } @Test - @DisplayName("should fail with RepositoryFailure when repository fails for storageLocationId filter") - void shouldFailWhenRepositoryFailsForFilter() { - when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1"))) - .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); + @DisplayName("should return empty page when no counts match") + void shouldReturnEmptyPageWhenNoCountsMatch() { + when(inventoryCountRepository.findAll(eq(InventoryCountStatus.OPEN), eq(pageRequest))) + .thenReturn(Result.success(Page.empty(100))); - var result = listInventoryCounts.execute("location-1", null, actorId); - - assertThat(result.isFailure()).isTrue(); - assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class); - } - - @Test - @DisplayName("should return empty list when no counts match") - void shouldReturnEmptyListWhenNoCountsMatch() { - when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("unknown"))) - .thenReturn(Result.success(List.of())); - - var result = listInventoryCounts.execute("unknown", null, actorId); + var result = listInventoryCounts.execute("OPEN", actorId, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); - } - - @Test - @DisplayName("should fail with InvalidStorageLocationId for blank storageLocationId") - void shouldFailWhenBlankStorageLocationId() { - var result = listInventoryCounts.execute(" ", null, actorId); - - assertThat(result.isFailure()).isTrue(); - assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStorageLocationId.class); + assertThat(result.unsafeGetValue().content()).isEmpty(); } @Test @DisplayName("should filter by status only") void shouldFilterByStatusOnly() { - when(inventoryCountRepository.findByStatus(InventoryCountStatus.OPEN)) - .thenReturn(Result.success(List.of(count1))); + when(inventoryCountRepository.findAll(eq(InventoryCountStatus.OPEN), eq(pageRequest))) + .thenReturn(Result.success(Page.of(List.of(count1), 0, 100, 1))); - var result = listInventoryCounts.execute(null, "OPEN", actorId); + var result = listInventoryCounts.execute("OPEN", actorId, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - verify(inventoryCountRepository).findByStatus(InventoryCountStatus.OPEN); - verify(inventoryCountRepository, never()).findAll(); - } - - @Test - @DisplayName("should filter by storageLocationId and status") - void shouldFilterByStorageLocationIdAndStatus() { - when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1"))) - .thenReturn(Result.success(List.of(count1, count2))); - - var result = listInventoryCounts.execute("location-1", "OPEN", actorId); - - assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - assertThat(result.unsafeGetValue().getFirst().status()).isEqualTo(InventoryCountStatus.OPEN); + assertThat(result.unsafeGetValue().content()).hasSize(1); } @Test @DisplayName("should fail with InvalidStatus for invalid status string") void shouldFailWhenInvalidStatus() { - var result = listInventoryCounts.execute(null, "INVALID", actorId); + var result = listInventoryCounts.execute("INVALID", actorId, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatus.class); @@ -175,7 +130,7 @@ class ListInventoryCountsTest { reset(authPort); when(authPort.can(any(ActorId.class), any())).thenReturn(false); - var result = listInventoryCounts.execute(null, null, actorId); + var result = listInventoryCounts.execute(null, actorId, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.Unauthorized.class); diff --git a/backend/src/test/java/de/effigenix/application/inventory/ListStockMovementsTest.java b/backend/src/test/java/de/effigenix/application/inventory/ListStockMovementsTest.java index 4677df7..2f6c594 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/ListStockMovementsTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/ListStockMovementsTest.java @@ -2,6 +2,8 @@ package de.effigenix.application.inventory; import de.effigenix.domain.inventory.*; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -36,6 +38,7 @@ class ListStockMovementsTest { private ListStockMovements useCase; private StockMovement sampleMovement; private final ActorId actor = ActorId.of("user-1"); + private final PageRequest pageRequest = PageRequest.of(0, 100); @BeforeEach void setUp() { @@ -62,33 +65,34 @@ class ListStockMovementsTest { @Test @DisplayName("should return all movements when no filter") void shouldReturnAll() { - when(repository.findAll()).thenReturn(Result.success(List.of(sampleMovement))); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1))); - var result = useCase.execute(null, null, null, null, null, null, actor); + var result = useCase.execute(null, null, null, null, null, null, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - verify(repository).findAll(); + assertThat(result.unsafeGetValue().content()).hasSize(1); } @Test @DisplayName("should return empty list when no movements exist") void shouldReturnEmptyList() { - when(repository.findAll()).thenReturn(Result.success(List.of())); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.empty(100))); - var result = useCase.execute(null, null, null, null, null, null, actor); + var result = useCase.execute(null, null, null, null, null, null, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); + assertThat(result.unsafeGetValue().content()).isEmpty(); } @Test @DisplayName("should fail when repository fails") void shouldFailWhenRepositoryFails() { - when(repository.findAll()).thenReturn( - Result.failure(new RepositoryError.DatabaseError("connection lost"))); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = useCase.execute(null, null, null, null, null, null, actor); + var result = useCase.execute(null, null, null, null, null, null, actor, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.RepositoryFailure.class); @@ -102,21 +106,19 @@ class ListStockMovementsTest { @Test @DisplayName("should filter by stockId") void shouldFilterByStockId() { - when(repository.findAllByStockId(StockId.of("stock-1"))) - .thenReturn(Result.success(List.of(sampleMovement))); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1))); - var result = useCase.execute("stock-1", null, null, null, null, null, actor); + var result = useCase.execute("stock-1", null, null, null, null, null, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - verify(repository).findAllByStockId(StockId.of("stock-1")); - verify(repository, never()).findAll(); + assertThat(result.unsafeGetValue().content()).hasSize(1); } @Test @DisplayName("should fail with InvalidStockId when format invalid") void shouldFailWhenStockIdInvalid() { - var result = useCase.execute(" ", null, null, null, null, null, actor); + var result = useCase.execute(" ", null, null, null, null, null, actor, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidStockId.class); @@ -130,20 +132,19 @@ class ListStockMovementsTest { @Test @DisplayName("should filter by articleId") void shouldFilterByArticleId() { - when(repository.findAllByArticleId(ArticleId.of("article-1"))) - .thenReturn(Result.success(List.of(sampleMovement))); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1))); - var result = useCase.execute(null, "article-1", null, null, null, null, actor); + var result = useCase.execute(null, "article-1", null, null, null, null, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - verify(repository).findAllByArticleId(ArticleId.of("article-1")); + assertThat(result.unsafeGetValue().content()).hasSize(1); } @Test @DisplayName("should fail with InvalidArticleId when format invalid") void shouldFailWhenArticleIdInvalid() { - var result = useCase.execute(null, " ", null, null, null, null, actor); + var result = useCase.execute(null, " ", null, null, null, null, actor, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidArticleId.class); @@ -157,32 +158,31 @@ class ListStockMovementsTest { @Test @DisplayName("should filter by batchReference") void shouldFilterByBatchReference() { - when(repository.findAllByBatchReference("CHARGE-001")) - .thenReturn(Result.success(List.of(sampleMovement))); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1))); - var result = useCase.execute(null, null, null, "CHARGE-001", null, null, actor); + var result = useCase.execute(null, null, null, "CHARGE-001", null, null, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - verify(repository).findAllByBatchReference("CHARGE-001"); + assertThat(result.unsafeGetValue().content()).hasSize(1); } @Test @DisplayName("should return empty list when no movements for batch") void shouldReturnEmptyForUnknownBatch() { - when(repository.findAllByBatchReference("UNKNOWN")) - .thenReturn(Result.success(List.of())); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.empty(100))); - var result = useCase.execute(null, null, null, "UNKNOWN", null, null, actor); + var result = useCase.execute(null, null, null, "UNKNOWN", null, null, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); + assertThat(result.unsafeGetValue().content()).isEmpty(); } @Test @DisplayName("should fail with InvalidBatchReference when blank") void shouldFailWhenBatchReferenceBlank() { - var result = useCase.execute(null, null, null, " ", null, null, actor); + var result = useCase.execute(null, null, null, " ", null, null, actor, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidBatchReference.class); @@ -191,10 +191,10 @@ class ListStockMovementsTest { @Test @DisplayName("should fail when repository fails for batchReference") void shouldFailWhenRepositoryFailsForBatch() { - when(repository.findAllByBatchReference("CHARGE-001")) + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = useCase.execute(null, null, null, "CHARGE-001", null, null, actor); + var result = useCase.execute(null, null, null, "CHARGE-001", null, null, actor, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.RepositoryFailure.class); @@ -208,20 +208,19 @@ class ListStockMovementsTest { @Test @DisplayName("should filter by movementType") void shouldFilterByMovementType() { - when(repository.findAllByMovementType(MovementType.GOODS_RECEIPT)) - .thenReturn(Result.success(List.of(sampleMovement))); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1))); - var result = useCase.execute(null, null, "GOODS_RECEIPT", null, null, null, actor); + var result = useCase.execute(null, null, "GOODS_RECEIPT", null, null, null, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - verify(repository).findAllByMovementType(MovementType.GOODS_RECEIPT); + assertThat(result.unsafeGetValue().content()).hasSize(1); } @Test @DisplayName("should fail with InvalidMovementType when type invalid") void shouldFailWhenMovementTypeInvalid() { - var result = useCase.execute(null, null, "INVALID_TYPE", null, null, null, actor); + var result = useCase.execute(null, null, "INVALID_TYPE", null, null, null, actor, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidMovementType.class); @@ -238,69 +237,64 @@ class ListStockMovementsTest { @Test @DisplayName("should filter by from and to") void shouldFilterByFromAndTo() { - when(repository.findAllByPerformedAtBetween(from, to)) - .thenReturn(Result.success(List.of(sampleMovement))); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1))); - var result = useCase.execute(null, null, null, null, from, to, actor); + var result = useCase.execute(null, null, null, null, from, to, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - verify(repository).findAllByPerformedAtBetween(from, to); + assertThat(result.unsafeGetValue().content()).hasSize(1); } @Test @DisplayName("should filter with only from (open-ended)") void shouldFilterByFromOnly() { - when(repository.findAllByPerformedAtAfter(from)) - .thenReturn(Result.success(List.of(sampleMovement))); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1))); - var result = useCase.execute(null, null, null, null, from, null, actor); + var result = useCase.execute(null, null, null, null, from, null, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - verify(repository).findAllByPerformedAtAfter(from); } @Test @DisplayName("should filter with only to (open-ended)") void shouldFilterByToOnly() { - when(repository.findAllByPerformedAtBefore(to)) - .thenReturn(Result.success(List.of(sampleMovement))); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1))); - var result = useCase.execute(null, null, null, null, null, to, actor); + var result = useCase.execute(null, null, null, null, null, to, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - verify(repository).findAllByPerformedAtBefore(to); } @Test @DisplayName("should fail with InvalidDateRange when from is after to") void shouldFailWhenFromAfterTo() { - var result = useCase.execute(null, null, null, null, to, from, actor); + var result = useCase.execute(null, null, null, null, to, from, actor, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidDateRange.class); - verify(repository, never()).findAllByPerformedAtBetween(any(), any()); } @Test @DisplayName("should succeed when from equals to (same instant)") void shouldSucceedWhenFromEqualsTo() { - when(repository.findAllByPerformedAtBetween(from, from)) - .thenReturn(Result.success(List.of())); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.empty(100))); - var result = useCase.execute(null, null, null, null, from, from, actor); + var result = useCase.execute(null, null, null, null, from, from, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - verify(repository).findAllByPerformedAtBetween(from, from); } @Test @DisplayName("should fail when repository fails for date range") void shouldFailWhenRepositoryFailsForDateRange() { - when(repository.findAllByPerformedAtBetween(from, to)) + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = useCase.execute(null, null, null, null, from, to, actor); + var result = useCase.execute(null, null, null, null, from, to, actor, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.RepositoryFailure.class); @@ -314,88 +308,73 @@ class ListStockMovementsTest { @Test @DisplayName("stockId takes priority over articleId, batchReference and movementType") void stockIdTakesPriority() { - when(repository.findAllByStockId(StockId.of("stock-1"))) - .thenReturn(Result.success(List.of())); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.empty(100))); - var result = useCase.execute("stock-1", "article-1", "GOODS_RECEIPT", "CHARGE-001", null, null, actor); + var result = useCase.execute("stock-1", "article-1", "GOODS_RECEIPT", "CHARGE-001", null, null, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - verify(repository).findAllByStockId(StockId.of("stock-1")); - verify(repository, never()).findAllByArticleId(any()); - verify(repository, never()).findAllByBatchReference(any()); - verify(repository, never()).findAllByMovementType(any()); } @Test @DisplayName("articleId takes priority over batchReference and movementType") void articleIdTakesPriorityOverBatchAndMovementType() { - when(repository.findAllByArticleId(ArticleId.of("article-1"))) - .thenReturn(Result.success(List.of())); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.empty(100))); - var result = useCase.execute(null, "article-1", "GOODS_RECEIPT", "CHARGE-001", null, null, actor); + var result = useCase.execute(null, "article-1", "GOODS_RECEIPT", "CHARGE-001", null, null, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - verify(repository).findAllByArticleId(ArticleId.of("article-1")); - verify(repository, never()).findAllByBatchReference(any()); - verify(repository, never()).findAllByMovementType(any()); } @Test @DisplayName("batchReference takes priority over movementType") void batchReferenceTakesPriorityOverMovementType() { - when(repository.findAllByBatchReference("CHARGE-001")) - .thenReturn(Result.success(List.of())); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.empty(100))); - var result = useCase.execute(null, null, "GOODS_RECEIPT", "CHARGE-001", null, null, actor); + var result = useCase.execute(null, null, "GOODS_RECEIPT", "CHARGE-001", null, null, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - verify(repository).findAllByBatchReference("CHARGE-001"); - verify(repository, never()).findAllByMovementType(any()); } @Test @DisplayName("movementType takes priority over from/to") void movementTypeTakesPriorityOverDateRange() { - when(repository.findAllByMovementType(MovementType.GOODS_RECEIPT)) - .thenReturn(Result.success(List.of())); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.empty(100))); Instant from = Instant.parse("2026-01-01T00:00:00Z"); Instant to = Instant.parse("2026-12-31T23:59:59Z"); - var result = useCase.execute(null, null, "GOODS_RECEIPT", null, from, to, actor); + var result = useCase.execute(null, null, "GOODS_RECEIPT", null, from, to, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - verify(repository).findAllByMovementType(MovementType.GOODS_RECEIPT); - verify(repository, never()).findAllByPerformedAtBetween(any(), any()); } @Test @DisplayName("stockId takes priority over from/to") void stockIdTakesPriorityOverDateRange() { - when(repository.findAllByStockId(StockId.of("stock-1"))) - .thenReturn(Result.success(List.of())); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.empty(100))); Instant from = Instant.parse("2026-01-01T00:00:00Z"); Instant to = Instant.parse("2026-12-31T23:59:59Z"); - var result = useCase.execute("stock-1", null, null, null, from, to, actor); + var result = useCase.execute("stock-1", null, null, null, from, to, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - verify(repository).findAllByStockId(StockId.of("stock-1")); - verify(repository, never()).findAllByPerformedAtBetween(any(), any()); } @Test @DisplayName("batchReference takes priority over from/to") void batchReferenceTakesPriorityOverDateRange() { - when(repository.findAllByBatchReference("CHARGE-001")) - .thenReturn(Result.success(List.of())); + when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class))) + .thenReturn(Result.success(Page.empty(100))); Instant from = Instant.parse("2026-01-01T00:00:00Z"); Instant to = Instant.parse("2026-12-31T23:59:59Z"); - var result = useCase.execute(null, null, null, "CHARGE-001", from, to, actor); + var result = useCase.execute(null, null, null, "CHARGE-001", from, to, actor, pageRequest); assertThat(result.isSuccess()).isTrue(); - verify(repository).findAllByBatchReference("CHARGE-001"); - verify(repository, never()).findAllByPerformedAtBetween(any(), any()); } } @@ -408,11 +387,11 @@ class ListStockMovementsTest { void shouldFailWhenUnauthorized() { when(authPort.can(actor, InventoryAction.STOCK_MOVEMENT_READ)).thenReturn(false); - var result = useCase.execute(null, null, null, null, null, null, actor); + var result = useCase.execute(null, null, null, null, null, null, actor, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.Unauthorized.class); - verify(repository, never()).findAll(); + verify(repository, never()).findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)); } } } diff --git a/backend/src/test/java/de/effigenix/application/inventory/ListStocksBelowMinimumTest.java b/backend/src/test/java/de/effigenix/application/inventory/ListStocksBelowMinimumTest.java index 123b724..054bf40 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/ListStocksBelowMinimumTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/ListStocksBelowMinimumTest.java @@ -2,6 +2,8 @@ package de.effigenix.application.inventory; import de.effigenix.domain.inventory.*; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -140,12 +142,16 @@ class ListStocksBelowMinimumTest { // Unused methods for this test @Override public Result> findById(StockId id) { return Result.success(Optional.empty()); } + @Override public Result> findAll(PageRequest pageRequest) { return Result.success(Page.empty(pageRequest.size())); } + @Override public Result> findAllByStorageLocationId(StorageLocationId storageLocationId, PageRequest pageRequest) { return Result.success(Page.empty(pageRequest.size())); } + @Override public Result> findAllByArticleId(ArticleId articleId, PageRequest pageRequest) { return Result.success(Page.empty(pageRequest.size())); } @Override public Result> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(Optional.empty()); } @Override public Result existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(false); } @Override public Result> findAll() { return Result.success(List.of()); } @Override public Result> findAllByStorageLocationId(StorageLocationId storageLocationId) { return Result.success(List.of()); } @Override public Result> findAllByArticleId(ArticleId articleId) { return Result.success(List.of()); } @Override public Result> findAllWithExpiryRelevantBatches(LocalDate referenceDate) { return Result.success(List.of()); } + @Override public Result> findAllByBatchId(String batchId) { return Result.success(List.of()); } @Override public Result save(Stock stock) { return Result.success(null); } } } diff --git a/backend/src/test/java/de/effigenix/application/inventory/ListStocksTest.java b/backend/src/test/java/de/effigenix/application/inventory/ListStocksTest.java index defb616..4cc1557 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/ListStocksTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/ListStocksTest.java @@ -2,6 +2,8 @@ package de.effigenix.application.inventory; import de.effigenix.domain.inventory.*; import de.effigenix.domain.masterdata.article.ArticleId; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.junit.jupiter.api.BeforeEach; @@ -14,6 +16,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -48,47 +52,51 @@ class ListStocksTest { @Test @DisplayName("should return all stocks when no filter provided") void shouldReturnAllStocksWhenNoFilter() { - when(stockRepository.findAll()).thenReturn(Result.success(List.of(stock1, stock2))); + var pageRequest = PageRequest.of(0, 100); + when(stockRepository.findAll(any(PageRequest.class))) + .thenReturn(Result.success(Page.of(List.of(stock1, stock2), 0, 100, 2))); - var result = listStocks.execute(null, null); + var result = listStocks.execute(null, null, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); - verify(stockRepository).findAll(); + assertThat(result.unsafeGetValue().content()).hasSize(2); + verify(stockRepository).findAll(pageRequest); } @Test @DisplayName("should filter by storageLocationId") void shouldFilterByStorageLocationId() { - when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1"))) - .thenReturn(Result.success(List.of(stock1, stock2))); + var pageRequest = PageRequest.of(0, 100); + when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1"), pageRequest)) + .thenReturn(Result.success(Page.of(List.of(stock1, stock2), 0, 100, 2))); - var result = listStocks.execute("location-1", null); + var result = listStocks.execute("location-1", null, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); - verify(stockRepository).findAllByStorageLocationId(StorageLocationId.of("location-1")); - verify(stockRepository, never()).findAll(); + assertThat(result.unsafeGetValue().content()).hasSize(2); + verify(stockRepository).findAllByStorageLocationId(StorageLocationId.of("location-1"), pageRequest); + verify(stockRepository, never()).findAll(any(PageRequest.class)); } @Test @DisplayName("should filter by articleId") void shouldFilterByArticleId() { - when(stockRepository.findAllByArticleId(ArticleId.of("article-1"))) - .thenReturn(Result.success(List.of(stock1))); + var pageRequest = PageRequest.of(0, 100); + when(stockRepository.findAllByArticleId(ArticleId.of("article-1"), pageRequest)) + .thenReturn(Result.success(Page.of(List.of(stock1), 0, 100, 1))); - var result = listStocks.execute(null, "article-1"); + var result = listStocks.execute(null, "article-1", pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - verify(stockRepository).findAllByArticleId(ArticleId.of("article-1")); - verify(stockRepository, never()).findAll(); + assertThat(result.unsafeGetValue().content()).hasSize(1); + verify(stockRepository).findAllByArticleId(ArticleId.of("article-1"), pageRequest); + verify(stockRepository, never()).findAll(any(PageRequest.class)); } @Test @DisplayName("should fail when both filters are provided") void shouldFailWhenBothFiltersProvided() { - var result = listStocks.execute("location-1", "article-1"); + var result = listStocks.execute("location-1", "article-1", PageRequest.of(0, 100)); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockError.InvalidFilterCombination.class); @@ -98,10 +106,11 @@ class ListStocksTest { @Test @DisplayName("should fail with RepositoryFailure when repository fails for findAll") void shouldFailWhenRepositoryFailsForFindAll() { - when(stockRepository.findAll()) + var pageRequest = PageRequest.of(0, 100); + when(stockRepository.findAll(any(PageRequest.class))) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = listStocks.execute(null, null); + var result = listStocks.execute(null, null, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class); @@ -110,10 +119,11 @@ class ListStocksTest { @Test @DisplayName("should fail with RepositoryFailure when repository fails for storageLocationId filter") void shouldFailWhenRepositoryFailsForStorageLocationFilter() { - when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1"))) + var pageRequest = PageRequest.of(0, 100); + when(stockRepository.findAllByStorageLocationId(eq(StorageLocationId.of("location-1")), any(PageRequest.class))) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = listStocks.execute("location-1", null); + var result = listStocks.execute("location-1", null, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class); @@ -122,10 +132,11 @@ class ListStocksTest { @Test @DisplayName("should fail with RepositoryFailure when repository fails for articleId filter") void shouldFailWhenRepositoryFailsForArticleIdFilter() { - when(stockRepository.findAllByArticleId(ArticleId.of("article-1"))) + var pageRequest = PageRequest.of(0, 100); + when(stockRepository.findAllByArticleId(eq(ArticleId.of("article-1")), any(PageRequest.class))) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = listStocks.execute(null, "article-1"); + var result = listStocks.execute(null, "article-1", pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class); @@ -134,12 +145,13 @@ class ListStocksTest { @Test @DisplayName("should return empty list when no stocks match") void shouldReturnEmptyListWhenNoStocksMatch() { - when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("unknown"))) - .thenReturn(Result.success(List.of())); + var pageRequest = PageRequest.of(0, 100); + when(stockRepository.findAllByStorageLocationId(eq(StorageLocationId.of("unknown")), any(PageRequest.class))) + .thenReturn(Result.success(Page.empty(100))); - var result = listStocks.execute("unknown", null); + var result = listStocks.execute("unknown", null, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); + assertThat(result.unsafeGetValue().content()).isEmpty(); } } diff --git a/backend/src/test/java/de/effigenix/application/inventory/ListStorageLocationsTest.java b/backend/src/test/java/de/effigenix/application/inventory/ListStorageLocationsTest.java index 28869b3..1abd5ca 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/ListStorageLocationsTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/ListStorageLocationsTest.java @@ -1,6 +1,8 @@ package de.effigenix.application.inventory; import de.effigenix.domain.inventory.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.junit.jupiter.api.BeforeEach; @@ -14,6 +16,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -58,32 +63,37 @@ class ListStorageLocationsTest { activeLocation("Lager A", StorageType.DRY_STORAGE), inactiveLocation("Lager B", StorageType.COLD_ROOM) ); - when(storageLocationRepository.findAll()).thenReturn(Result.success(locations)); + var pageRequest = PageRequest.of(0, 100); + when(storageLocationRepository.findAll(isNull(), isNull(), eq(pageRequest))) + .thenReturn(Result.success(Page.of(locations, 0, 100, 2))); - var result = listStorageLocations.execute(null, null); + var result = listStorageLocations.execute(null, null, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); + assertThat(result.unsafeGetValue().content()).hasSize(2); } @Test @DisplayName("should return empty list when none exist") void shouldReturnEmptyList() { - when(storageLocationRepository.findAll()).thenReturn(Result.success(List.of())); + var pageRequest = PageRequest.of(0, 100); + when(storageLocationRepository.findAll(isNull(), isNull(), eq(pageRequest))) + .thenReturn(Result.success(Page.empty(100))); - var result = listStorageLocations.execute(null, null); + var result = listStorageLocations.execute(null, null, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); + assertThat(result.unsafeGetValue().content()).isEmpty(); } @Test @DisplayName("should fail when repository fails") void shouldFailWhenRepositoryFails() { - when(storageLocationRepository.findAll()) + var pageRequest = PageRequest.of(0, 100); + when(storageLocationRepository.findAll(isNull(), isNull(), eq(pageRequest))) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = listStorageLocations.execute(null, null); + var result = listStorageLocations.execute(null, null, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.RepositoryFailure.class); @@ -99,47 +109,44 @@ class ListStorageLocationsTest { @Test @DisplayName("should return only active locations when active=true") void shouldReturnOnlyActive() { - var locations = List.of( - activeLocation("Aktiv", StorageType.DRY_STORAGE), - inactiveLocation("Inaktiv", StorageType.COLD_ROOM) - ); - when(storageLocationRepository.findAll()).thenReturn(Result.success(locations)); + var activeLocations = List.of(activeLocation("Aktiv", StorageType.DRY_STORAGE)); + var pageRequest = PageRequest.of(0, 100); + when(storageLocationRepository.findAll(isNull(), eq(true), eq(pageRequest))) + .thenReturn(Result.success(Page.of(activeLocations, 0, 100, 1))); - var result = listStorageLocations.execute(null, true); + var result = listStorageLocations.execute(null, true, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Aktiv"); + assertThat(result.unsafeGetValue().content()).hasSize(1); + assertThat(result.unsafeGetValue().content().get(0).name().value()).isEqualTo("Aktiv"); } @Test @DisplayName("should return only inactive locations when active=false") void shouldReturnOnlyInactive() { - var locations = List.of( - activeLocation("Aktiv", StorageType.DRY_STORAGE), - inactiveLocation("Inaktiv", StorageType.COLD_ROOM) - ); - when(storageLocationRepository.findAll()).thenReturn(Result.success(locations)); + var inactiveLocations = List.of(inactiveLocation("Inaktiv", StorageType.COLD_ROOM)); + var pageRequest = PageRequest.of(0, 100); + when(storageLocationRepository.findAll(isNull(), eq(false), eq(pageRequest))) + .thenReturn(Result.success(Page.of(inactiveLocations, 0, 100, 1))); - var result = listStorageLocations.execute(null, false); + var result = listStorageLocations.execute(null, false, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Inaktiv"); + assertThat(result.unsafeGetValue().content()).hasSize(1); + assertThat(result.unsafeGetValue().content().get(0).name().value()).isEqualTo("Inaktiv"); } @Test @DisplayName("should return empty list when no locations match active filter") void shouldReturnEmptyWhenNoMatch() { - var locations = List.of( - activeLocation("Aktiv", StorageType.DRY_STORAGE) - ); - when(storageLocationRepository.findAll()).thenReturn(Result.success(locations)); + var pageRequest = PageRequest.of(0, 100); + when(storageLocationRepository.findAll(isNull(), eq(false), eq(pageRequest))) + .thenReturn(Result.success(Page.empty(100))); - var result = listStorageLocations.execute(null, false); + var result = listStorageLocations.execute(null, false, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); + assertThat(result.unsafeGetValue().content()).isEmpty(); } } @@ -152,23 +159,22 @@ class ListStorageLocationsTest { @Test @DisplayName("should return locations of given storage type") void shouldReturnByStorageType() { - var coldRooms = List.of( - activeLocation("Kühlraum", StorageType.COLD_ROOM) - ); - when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM)) - .thenReturn(Result.success(coldRooms)); + var coldRooms = List.of(activeLocation("Kühlraum", StorageType.COLD_ROOM)); + var pageRequest = PageRequest.of(0, 100); + when(storageLocationRepository.findAll(eq(StorageType.COLD_ROOM), isNull(), eq(pageRequest))) + .thenReturn(Result.success(Page.of(coldRooms, 0, 100, 1))); - var result = listStorageLocations.execute("COLD_ROOM", null); + var result = listStorageLocations.execute("COLD_ROOM", null, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - assertThat(result.unsafeGetValue().get(0).storageType()).isEqualTo(StorageType.COLD_ROOM); + assertThat(result.unsafeGetValue().content()).hasSize(1); + assertThat(result.unsafeGetValue().content().get(0).storageType()).isEqualTo(StorageType.COLD_ROOM); } @Test @DisplayName("should fail with InvalidStorageType for unknown type") void shouldFailForInvalidStorageType() { - var result = listStorageLocations.execute("INVALID", null); + var result = listStorageLocations.execute("INVALID", null, PageRequest.of(0, 100)); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.InvalidStorageType.class); @@ -185,50 +191,44 @@ class ListStorageLocationsTest { @Test @DisplayName("should return only active locations of given type") void shouldReturnActiveOfType() { - var coldRooms = List.of( - activeLocation("Kühl Aktiv", StorageType.COLD_ROOM), - inactiveLocation("Kühl Inaktiv", StorageType.COLD_ROOM) - ); - when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM)) - .thenReturn(Result.success(coldRooms)); + var activeOfType = List.of(activeLocation("Kühl Aktiv", StorageType.COLD_ROOM)); + var pageRequest = PageRequest.of(0, 100); + when(storageLocationRepository.findAll(eq(StorageType.COLD_ROOM), eq(true), eq(pageRequest))) + .thenReturn(Result.success(Page.of(activeOfType, 0, 100, 1))); - var result = listStorageLocations.execute("COLD_ROOM", true); + var result = listStorageLocations.execute("COLD_ROOM", true, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Kühl Aktiv"); + assertThat(result.unsafeGetValue().content()).hasSize(1); + assertThat(result.unsafeGetValue().content().get(0).name().value()).isEqualTo("Kühl Aktiv"); } @Test @DisplayName("should return only inactive locations of given type") void shouldReturnInactiveOfType() { - var coldRooms = List.of( - activeLocation("Kühl Aktiv", StorageType.COLD_ROOM), - inactiveLocation("Kühl Inaktiv", StorageType.COLD_ROOM) - ); - when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM)) - .thenReturn(Result.success(coldRooms)); + var inactiveOfType = List.of(inactiveLocation("Kühl Inaktiv", StorageType.COLD_ROOM)); + var pageRequest = PageRequest.of(0, 100); + when(storageLocationRepository.findAll(eq(StorageType.COLD_ROOM), eq(false), eq(pageRequest))) + .thenReturn(Result.success(Page.of(inactiveOfType, 0, 100, 1))); - var result = listStorageLocations.execute("COLD_ROOM", false); + var result = listStorageLocations.execute("COLD_ROOM", false, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Kühl Inaktiv"); + assertThat(result.unsafeGetValue().content()).hasSize(1); + assertThat(result.unsafeGetValue().content().get(0).name().value()).isEqualTo("Kühl Inaktiv"); } @Test @DisplayName("should return empty list when no locations match combined filter") void shouldReturnEmptyWhenNoMatchCombined() { - var coldRooms = List.of( - activeLocation("Kühl Aktiv", StorageType.COLD_ROOM) - ); - when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM)) - .thenReturn(Result.success(coldRooms)); + var pageRequest = PageRequest.of(0, 100); + when(storageLocationRepository.findAll(eq(StorageType.COLD_ROOM), eq(false), eq(pageRequest))) + .thenReturn(Result.success(Page.empty(100))); - var result = listStorageLocations.execute("COLD_ROOM", false); + var result = listStorageLocations.execute("COLD_ROOM", false, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); + assertThat(result.unsafeGetValue().content()).isEmpty(); } } } diff --git a/backend/src/test/java/de/effigenix/application/masterdata/ArticleUseCaseTest.java b/backend/src/test/java/de/effigenix/application/masterdata/ArticleUseCaseTest.java index c265b33..1bc08db 100644 --- a/backend/src/test/java/de/effigenix/application/masterdata/ArticleUseCaseTest.java +++ b/backend/src/test/java/de/effigenix/application/masterdata/ArticleUseCaseTest.java @@ -11,6 +11,8 @@ import de.effigenix.domain.masterdata.article.*; import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.domain.masterdata.supplier.SupplierId; import de.effigenix.shared.common.Money; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.persistence.UnitOfWork; @@ -419,58 +421,37 @@ class ArticleUseCaseTest { @DisplayName("should return all articles") void shouldReturnAllArticles() { var articles = List.of(reconstitutedArticle("a-1"), reconstitutedArticle("a-2")); - when(articleRepository.findAll()).thenReturn(Result.success(articles)); + var pageRequest = PageRequest.of(0, 100); + when(articleRepository.findAll(null, pageRequest)) + .thenReturn(Result.success(Page.of(articles, 0, 100, 2))); - var result = listArticles.execute(); + var result = listArticles.execute(null, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); + assertThat(result.unsafeGetValue().content()).hasSize(2); } @Test @DisplayName("should return empty list when none exist") void shouldReturnEmptyList() { - when(articleRepository.findAll()).thenReturn(Result.success(List.of())); + var pageRequest = PageRequest.of(0, 100); + when(articleRepository.findAll(null, pageRequest)) + .thenReturn(Result.success(Page.empty(100))); - var result = listArticles.execute(); + var result = listArticles.execute(null, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); + assertThat(result.unsafeGetValue().content()).isEmpty(); } @Test @DisplayName("should fail with RepositoryFailure when findAll fails") void shouldFailWhenRepositoryFails() { - when(articleRepository.findAll()) + var pageRequest = PageRequest.of(0, 100); + when(articleRepository.findAll(null, pageRequest)) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = listArticles.execute(); - - assertThat(result.isFailure()).isTrue(); - assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.RepositoryFailure.class); - } - - @Test - @DisplayName("should return articles by category") - void shouldReturnByCategory() { - var catId = ProductCategoryId.of(CATEGORY_ID); - var articles = List.of(reconstitutedArticle("a-1")); - when(articleRepository.findByCategory(catId)).thenReturn(Result.success(articles)); - - var result = listArticles.executeByCategory(catId); - - assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - } - - @Test - @DisplayName("should fail with RepositoryFailure when findByCategory fails") - void shouldFailWhenFindByCategoryFails() { - var catId = ProductCategoryId.of(CATEGORY_ID); - when(articleRepository.findByCategory(catId)) - .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - - var result = listArticles.executeByCategory(catId); + var result = listArticles.execute(null, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.RepositoryFailure.class); @@ -479,23 +460,25 @@ class ArticleUseCaseTest { @Test @DisplayName("should return articles by status") void shouldReturnByStatus() { + var pageRequest = PageRequest.of(0, 100); var articles = List.of(reconstitutedArticle("a-1")); - when(articleRepository.findByStatus(ArticleStatus.ACTIVE)) - .thenReturn(Result.success(articles)); + when(articleRepository.findAll(ArticleStatus.ACTIVE, pageRequest)) + .thenReturn(Result.success(Page.of(articles, 0, 100, 1))); - var result = listArticles.executeByStatus(ArticleStatus.ACTIVE); + var result = listArticles.execute(ArticleStatus.ACTIVE, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); + assertThat(result.unsafeGetValue().content()).hasSize(1); } @Test @DisplayName("should fail with RepositoryFailure when findByStatus fails") void shouldFailWhenFindByStatusFails() { - when(articleRepository.findByStatus(ArticleStatus.ACTIVE)) + var pageRequest = PageRequest.of(0, 100); + when(articleRepository.findAll(ArticleStatus.ACTIVE, pageRequest)) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = listArticles.executeByStatus(ArticleStatus.ACTIVE); + var result = listArticles.execute(ArticleStatus.ACTIVE, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.RepositoryFailure.class); diff --git a/backend/src/test/java/de/effigenix/application/masterdata/CustomerUseCaseTest.java b/backend/src/test/java/de/effigenix/application/masterdata/CustomerUseCaseTest.java index b91fcf8..e50d50b 100644 --- a/backend/src/test/java/de/effigenix/application/masterdata/CustomerUseCaseTest.java +++ b/backend/src/test/java/de/effigenix/application/masterdata/CustomerUseCaseTest.java @@ -5,7 +5,14 @@ import de.effigenix.application.masterdata.customer.command.*; import de.effigenix.domain.masterdata.*; import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.customer.*; -import de.effigenix.shared.common.*; +import de.effigenix.shared.common.Address; +import de.effigenix.shared.common.ContactInfo; +import de.effigenix.shared.common.Money; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; +import de.effigenix.shared.common.PaymentTerms; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; import de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.ActorId; import org.junit.jupiter.api.BeforeEach; @@ -386,81 +393,37 @@ class CustomerUseCaseTest { @DisplayName("should return all customers") void shouldReturnAllCustomers() { var customers = List.of(existingB2BCustomer("c1"), existingB2CCustomer("c2")); - when(customerRepository.findAll()).thenReturn(Result.success(customers)); + var pageRequest = PageRequest.of(0, 100); + when(customerRepository.findAll(pageRequest)) + .thenReturn(Result.success(Page.of(customers, 0, 100, 2))); - var result = listCustomers.execute(); + var result = listCustomers.execute(pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); + assertThat(result.unsafeGetValue().content()).hasSize(2); } @Test @DisplayName("should return empty list when no customers exist") void shouldReturnEmptyList() { - when(customerRepository.findAll()).thenReturn(Result.success(List.of())); + var pageRequest = PageRequest.of(0, 100); + when(customerRepository.findAll(pageRequest)) + .thenReturn(Result.success(Page.empty(100))); - var result = listCustomers.execute(); + var result = listCustomers.execute(pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); + assertThat(result.unsafeGetValue().content()).isEmpty(); } @Test @DisplayName("should fail with RepositoryFailure when findAll fails") void shouldFailWhenFindAllFails() { - when(customerRepository.findAll()) + var pageRequest = PageRequest.of(0, 100); + when(customerRepository.findAll(pageRequest)) .thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout"))); - var result = listCustomers.execute(); - - assertThat(result.isFailure()).isTrue(); - assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class); - } - - @Test - @DisplayName("should return customers by type") - void shouldReturnCustomersByType() { - var b2bCustomers = List.of(existingB2BCustomer("c1")); - when(customerRepository.findByType(CustomerType.B2B)).thenReturn(Result.success(b2bCustomers)); - - var result = listCustomers.executeByType(CustomerType.B2B); - - assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - assertThat(result.unsafeGetValue().getFirst().type()).isEqualTo(CustomerType.B2B); - } - - @Test - @DisplayName("should fail with RepositoryFailure when findByType fails") - void shouldFailWhenFindByTypeFails() { - when(customerRepository.findByType(CustomerType.B2B)) - .thenReturn(Result.failure(new RepositoryError.DatabaseError("error"))); - - var result = listCustomers.executeByType(CustomerType.B2B); - - assertThat(result.isFailure()).isTrue(); - assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class); - } - - @Test - @DisplayName("should return customers by status") - void shouldReturnCustomersByStatus() { - var activeCustomers = List.of(existingB2BCustomer("c1"), existingB2CCustomer("c2")); - when(customerRepository.findByStatus(CustomerStatus.ACTIVE)).thenReturn(Result.success(activeCustomers)); - - var result = listCustomers.executeByStatus(CustomerStatus.ACTIVE); - - assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); - } - - @Test - @DisplayName("should fail with RepositoryFailure when findByStatus fails") - void shouldFailWhenFindByStatusFails() { - when(customerRepository.findByStatus(CustomerStatus.ACTIVE)) - .thenReturn(Result.failure(new RepositoryError.DatabaseError("error"))); - - var result = listCustomers.executeByStatus(CustomerStatus.ACTIVE); + var result = listCustomers.execute(pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class); diff --git a/backend/src/test/java/de/effigenix/application/masterdata/ProductCategoryUseCaseTest.java b/backend/src/test/java/de/effigenix/application/masterdata/ProductCategoryUseCaseTest.java index 82defd0..fe9d1f0 100644 --- a/backend/src/test/java/de/effigenix/application/masterdata/ProductCategoryUseCaseTest.java +++ b/backend/src/test/java/de/effigenix/application/masterdata/ProductCategoryUseCaseTest.java @@ -9,6 +9,8 @@ import de.effigenix.application.masterdata.productcategory.UpdateProductCategory import de.effigenix.domain.masterdata.article.Article; import de.effigenix.domain.masterdata.article.ArticleRepository; import de.effigenix.domain.masterdata.productcategory.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.persistence.UnitOfWork; @@ -346,32 +348,37 @@ class ProductCategoryUseCaseTest { existingCategory("cat-1", "Backwaren"), existingCategory("cat-2", "Getränke") ); - when(categoryRepository.findAll()).thenReturn(Result.success(categories)); + var pageRequest = PageRequest.of(0, 100); + when(categoryRepository.findAll(pageRequest)) + .thenReturn(Result.success(Page.of(categories, 0, 100, 2))); - var result = useCase.execute(); + var result = useCase.execute(pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); + assertThat(result.unsafeGetValue().content()).hasSize(2); } @Test @DisplayName("should return empty list when no categories exist") void shouldReturnEmptyList() { - when(categoryRepository.findAll()).thenReturn(Result.success(List.of())); + var pageRequest = PageRequest.of(0, 100); + when(categoryRepository.findAll(pageRequest)) + .thenReturn(Result.success(Page.empty(100))); - var result = useCase.execute(); + var result = useCase.execute(pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); + assertThat(result.unsafeGetValue().content()).isEmpty(); } @Test @DisplayName("should fail with RepositoryFailure when repository fails") void shouldFailWhenRepositoryFails() { - when(categoryRepository.findAll()) + var pageRequest = PageRequest.of(0, 100); + when(categoryRepository.findAll(pageRequest)) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = useCase.execute(); + var result = useCase.execute(pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class); diff --git a/backend/src/test/java/de/effigenix/application/masterdata/SupplierUseCaseTest.java b/backend/src/test/java/de/effigenix/application/masterdata/SupplierUseCaseTest.java index 1e7b0fe..165d560 100644 --- a/backend/src/test/java/de/effigenix/application/masterdata/SupplierUseCaseTest.java +++ b/backend/src/test/java/de/effigenix/application/masterdata/SupplierUseCaseTest.java @@ -4,6 +4,8 @@ import de.effigenix.application.masterdata.supplier.*; import de.effigenix.application.masterdata.supplier.command.*; import de.effigenix.domain.masterdata.supplier.*; import de.effigenix.shared.common.ContactInfo; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.persistence.UnitOfWork; @@ -388,32 +390,37 @@ class SupplierUseCaseTest { @DisplayName("should return all suppliers") void shouldReturnAllSuppliers() { var suppliers = List.of(existingSupplier("s-1"), existingSupplier("s-2")); - when(supplierRepository.findAll()).thenReturn(Result.success(suppliers)); + var pageRequest = PageRequest.of(0, 100); + when(supplierRepository.findAll(null, pageRequest)) + .thenReturn(Result.success(Page.of(suppliers, 0, 100, 2))); - var result = listSuppliers.execute(); + var result = listSuppliers.execute(null, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); + assertThat(result.unsafeGetValue().content()).hasSize(2); } @Test @DisplayName("should return empty list when no suppliers exist") void shouldReturnEmptyList() { - when(supplierRepository.findAll()).thenReturn(Result.success(List.of())); + var pageRequest = PageRequest.of(0, 100); + when(supplierRepository.findAll(null, pageRequest)) + .thenReturn(Result.success(Page.empty(100))); - var result = listSuppliers.execute(); + var result = listSuppliers.execute(null, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); + assertThat(result.unsafeGetValue().content()).isEmpty(); } @Test @DisplayName("should fail with RepositoryFailure when findAll fails") void shouldFailWhenFindAllFails() { - when(supplierRepository.findAll()) + var pageRequest = PageRequest.of(0, 100); + when(supplierRepository.findAll(null, pageRequest)) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = listSuppliers.execute(); + var result = listSuppliers.execute(null, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class); @@ -423,22 +430,24 @@ class SupplierUseCaseTest { @DisplayName("should return suppliers filtered by status") void shouldReturnSuppliersByStatus() { var activeSuppliers = List.of(existingSupplier("s-1")); - when(supplierRepository.findByStatus(SupplierStatus.ACTIVE)) - .thenReturn(Result.success(activeSuppliers)); + var pageRequest = PageRequest.of(0, 100); + when(supplierRepository.findAll(SupplierStatus.ACTIVE, pageRequest)) + .thenReturn(Result.success(Page.of(activeSuppliers, 0, 100, 1))); - var result = listSuppliers.executeByStatus(SupplierStatus.ACTIVE); + var result = listSuppliers.execute(SupplierStatus.ACTIVE, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); + assertThat(result.unsafeGetValue().content()).hasSize(1); } @Test @DisplayName("should fail with RepositoryFailure when findByStatus fails") void shouldFailWhenFindByStatusFails() { - when(supplierRepository.findByStatus(SupplierStatus.ACTIVE)) + var pageRequest = PageRequest.of(0, 100); + when(supplierRepository.findAll(SupplierStatus.ACTIVE, pageRequest)) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = listSuppliers.executeByStatus(SupplierStatus.ACTIVE); + var result = listSuppliers.execute(SupplierStatus.ACTIVE, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class); diff --git a/backend/src/test/java/de/effigenix/application/production/ListBatchesTest.java b/backend/src/test/java/de/effigenix/application/production/ListBatchesTest.java index d47b50a..cd1e6b5 100644 --- a/backend/src/test/java/de/effigenix/application/production/ListBatchesTest.java +++ b/backend/src/test/java/de/effigenix/application/production/ListBatchesTest.java @@ -1,6 +1,8 @@ package de.effigenix.application.production; import de.effigenix.domain.production.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -80,34 +82,39 @@ class ListBatchesTest { @DisplayName("should return all batches") void should_ReturnAllBatches() { var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED), sampleBatch("b2", BatchStatus.PLANNED)); + var pageRequest = PageRequest.of(0, 100); when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true); - when(batchRepository.findAllSummary()).thenReturn(Result.success(batches)); + when(batchRepository.findAllSummary(pageRequest)) + .thenReturn(Result.success(Page.of(batches, 0, 100, 2))); - var result = listBatches.execute(performedBy); + var result = listBatches.execute(performedBy, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); + assertThat(result.unsafeGetValue().content()).hasSize(2); } @Test @DisplayName("should return empty list when no batches exist") void should_ReturnEmptyList_When_NoBatchesExist() { + var pageRequest = PageRequest.of(0, 100); when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true); - when(batchRepository.findAllSummary()).thenReturn(Result.success(List.of())); + when(batchRepository.findAllSummary(pageRequest)) + .thenReturn(Result.success(Page.empty(100))); - var result = listBatches.execute(performedBy); + var result = listBatches.execute(performedBy, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); + assertThat(result.unsafeGetValue().content()).isEmpty(); } @Test @DisplayName("should fail with RepositoryFailure when findAll fails") void should_FailWithRepositoryFailure_When_FindAllFails() { + var pageRequest = PageRequest.of(0, 100); when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true); - when(batchRepository.findAllSummary()).thenReturn(Result.failure(DB_ERROR)); + when(batchRepository.findAllSummary(pageRequest)).thenReturn(Result.failure(DB_ERROR)); - var result = listBatches.execute(performedBy); + var result = listBatches.execute(performedBy, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class); @@ -118,7 +125,7 @@ class ListBatchesTest { void should_FailWithUnauthorized() { when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false); - var result = listBatches.execute(performedBy); + var result = listBatches.execute(performedBy, PageRequest.of(0, 100)); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class); diff --git a/backend/src/test/java/de/effigenix/application/production/ListProductionOrdersTest.java b/backend/src/test/java/de/effigenix/application/production/ListProductionOrdersTest.java index f05de6d..171d3be 100644 --- a/backend/src/test/java/de/effigenix/application/production/ListProductionOrdersTest.java +++ b/backend/src/test/java/de/effigenix/application/production/ListProductionOrdersTest.java @@ -1,6 +1,8 @@ package de.effigenix.application.production; import de.effigenix.domain.production.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; @@ -61,14 +63,16 @@ class ListProductionOrdersTest { @Test @DisplayName("should list all orders") void should_ListAll() { + var pageRequest = PageRequest.of(0, 100); when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(true); - when(productionOrderRepository.findAll()).thenReturn(Result.success(List.of(sampleOrder()))); + when(productionOrderRepository.findAll(pageRequest)) + .thenReturn(Result.success(Page.of(List.of(sampleOrder()), 0, 100, 1))); - var result = listProductionOrders.execute(performedBy); + var result = listProductionOrders.execute(performedBy, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(1); - verify(productionOrderRepository).findAll(); + assertThat(result.unsafeGetValue().content()).hasSize(1); + verify(productionOrderRepository).findAll(pageRequest); } @Test @@ -118,7 +122,7 @@ class ListProductionOrdersTest { void should_Fail_When_Unauthorized() { when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(false); - var result = listProductionOrders.execute(performedBy); + var result = listProductionOrders.execute(performedBy, PageRequest.of(0, 100)); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.Unauthorized.class); @@ -128,11 +132,12 @@ class ListProductionOrdersTest { @Test @DisplayName("should fail when repository returns error") void should_Fail_When_RepositoryError() { + var pageRequest = PageRequest.of(0, 100); when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(true); - when(productionOrderRepository.findAll()) + when(productionOrderRepository.findAll(pageRequest)) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); - var result = listProductionOrders.execute(performedBy); + var result = listProductionOrders.execute(performedBy, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.RepositoryFailure.class); diff --git a/backend/src/test/java/de/effigenix/application/production/ListRecipesTest.java b/backend/src/test/java/de/effigenix/application/production/ListRecipesTest.java index fea301d..a0abfbc 100644 --- a/backend/src/test/java/de/effigenix/application/production/ListRecipesTest.java +++ b/backend/src/test/java/de/effigenix/application/production/ListRecipesTest.java @@ -1,6 +1,8 @@ package de.effigenix.application.production; import de.effigenix.domain.production.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Result; import de.effigenix.shared.common.UnitOfMeasure; @@ -54,13 +56,15 @@ class ListRecipesTest { recipeWithStatus("r1", RecipeStatus.DRAFT), recipeWithStatus("r2", RecipeStatus.ACTIVE) ); + var pageRequest = PageRequest.of(0, 100); when(authPort.can(performedBy, ProductionAction.RECIPE_READ)).thenReturn(true); - when(recipeRepository.findAll()).thenReturn(Result.success(recipes)); + when(recipeRepository.findAll(pageRequest)) + .thenReturn(Result.success(Page.of(recipes, 0, 100, 2))); - var result = listRecipes.execute(performedBy); + var result = listRecipes.execute(performedBy, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); + assertThat(result.unsafeGetValue().content()).hasSize(2); } @Test @@ -82,7 +86,7 @@ class ListRecipesTest { void should_FailWithUnauthorized_When_ActorLacksPermission() { when(authPort.can(performedBy, ProductionAction.RECIPE_READ)).thenReturn(false); - var result = listRecipes.execute(performedBy); + var result = listRecipes.execute(performedBy, PageRequest.of(0, 100)); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.Unauthorized.class); diff --git a/backend/src/test/java/de/effigenix/application/shared/ListCountriesTest.java b/backend/src/test/java/de/effigenix/application/shared/ListCountriesTest.java index 5ddf3c0..8828683 100644 --- a/backend/src/test/java/de/effigenix/application/shared/ListCountriesTest.java +++ b/backend/src/test/java/de/effigenix/application/shared/ListCountriesTest.java @@ -2,6 +2,8 @@ package de.effigenix.application.shared; import de.effigenix.shared.common.Country; import de.effigenix.shared.common.CountryRepository; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -23,12 +25,45 @@ class ListCountriesTest { @InjectMocks private ListCountries listCountries; + @Test + void shouldDelegateToFindAllWithPageRequest() { + var expected = List.of(new Country("DE", "Deutschland")); + var pageRequest = PageRequest.of(0, 100); + when(countryRepository.findAll(pageRequest)) + .thenReturn(Page.of(expected, 0, 100, 1)); + + var result = listCountries.execute(pageRequest); + + assertThat(result.content()).isEqualTo(expected); + verify(countryRepository).findAll(pageRequest); + } + + @Test + void shouldDelegateToSearchWhenQueryIsProvided() { + var expected = List.of(new Country("DE", "Deutschland")); + when(countryRepository.search("deutsch")).thenReturn(expected); + + var result = listCountries.search("deutsch"); + + assertThat(result).isEqualTo(expected); + verify(countryRepository).search("deutsch"); + } + + @Test + void shouldReturnEmptyListWhenNoMatch() { + when(countryRepository.search("xyz")).thenReturn(List.of()); + + var result = listCountries.search("xyz"); + + assertThat(result).isEmpty(); + } + @Test void shouldDelegateToFindAllWhenQueryIsNull() { var expected = List.of(new Country("DE", "Deutschland")); when(countryRepository.findAll()).thenReturn(expected); - var result = listCountries.execute(null); + var result = listCountries.search(null); assertThat(result).isEqualTo(expected); verify(countryRepository).findAll(); @@ -39,38 +74,18 @@ class ListCountriesTest { var expected = List.of(new Country("DE", "Deutschland")); when(countryRepository.findAll()).thenReturn(expected); - var result = listCountries.execute(" "); + var result = listCountries.search(" "); assertThat(result).isEqualTo(expected); verify(countryRepository).findAll(); } - @Test - void shouldDelegateToSearchWhenQueryIsProvided() { - var expected = List.of(new Country("DE", "Deutschland")); - when(countryRepository.search("deutsch")).thenReturn(expected); - - var result = listCountries.execute("deutsch"); - - assertThat(result).isEqualTo(expected); - verify(countryRepository).search("deutsch"); - } - - @Test - void shouldReturnEmptyListWhenNoMatch() { - when(countryRepository.search("xyz")).thenReturn(List.of()); - - var result = listCountries.execute("xyz"); - - assertThat(result).isEmpty(); - } - @Test void shouldDelegateToFindAllWhenQueryIsEmpty() { var expected = List.of(new Country("DE", "Deutschland")); when(countryRepository.findAll()).thenReturn(expected); - var result = listCountries.execute(""); + var result = listCountries.search(""); assertThat(result).isEqualTo(expected); verify(countryRepository).findAll(); diff --git a/backend/src/test/java/de/effigenix/application/usermanagement/ListUsersTest.java b/backend/src/test/java/de/effigenix/application/usermanagement/ListUsersTest.java index 5ed91a3..24e4e79 100644 --- a/backend/src/test/java/de/effigenix/application/usermanagement/ListUsersTest.java +++ b/backend/src/test/java/de/effigenix/application/usermanagement/ListUsersTest.java @@ -2,6 +2,8 @@ package de.effigenix.application.usermanagement; import de.effigenix.application.usermanagement.dto.UserDTO; import de.effigenix.domain.usermanagement.*; +import de.effigenix.shared.common.Page; +import de.effigenix.shared.common.PageRequest; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.AuthorizationPort; @@ -32,6 +34,7 @@ class ListUsersTest { private ActorId performedBy; private User user1; private User user2; + private final PageRequest pageRequest = PageRequest.of(0, 100); @BeforeEach void setUp() { @@ -53,13 +56,14 @@ class ListUsersTest { @DisplayName("should_ReturnAllUsers_When_Authorized") void should_ReturnAllUsers_When_Authorized() { when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(true); - when(userRepository.findAll()).thenReturn(Result.success(List.of(user1, user2))); + when(userRepository.findAll(null, pageRequest)) + .thenReturn(Result.success(Page.of(List.of(user1, user2), 0, 100, 2))); - Result> result = listUsers.execute(performedBy); + Result> result = listUsers.execute(null, performedBy, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); - assertThat(result.unsafeGetValue()).extracting(UserDTO::username) + assertThat(result.unsafeGetValue().content()).hasSize(2); + assertThat(result.unsafeGetValue().content()).extracting(UserDTO::username) .containsExactlyInAnyOrder("john.doe", "jane.doe"); } @@ -67,12 +71,13 @@ class ListUsersTest { @DisplayName("should_ReturnEmptyList_When_NoUsersExist") void should_ReturnEmptyList_When_NoUsersExist() { when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(true); - when(userRepository.findAll()).thenReturn(Result.success(List.of())); + when(userRepository.findAll(null, pageRequest)) + .thenReturn(Result.success(Page.empty(100))); - Result> result = listUsers.execute(performedBy); + Result> result = listUsers.execute(null, performedBy, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).isEmpty(); + assertThat(result.unsafeGetValue().content()).isEmpty(); } @Test @@ -80,23 +85,24 @@ class ListUsersTest { void should_FailWithUnauthorized_When_ActorLacksPermission() { when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(false); - Result> result = listUsers.execute(performedBy); + Result> result = listUsers.execute(null, performedBy, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class); - verify(userRepository, never()).findAll(); + verify(userRepository, never()).findAll(any(), any()); } @Test @DisplayName("should_ReturnBranchUsers_When_FilteredByBranch") void should_ReturnBranchUsers_When_FilteredByBranch() { when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(true); - when(userRepository.findByBranchId("branch-1")).thenReturn(Result.success(List.of(user1, user2))); + when(userRepository.findAll("branch-1", pageRequest)) + .thenReturn(Result.success(Page.of(List.of(user1, user2), 0, 100, 2))); - Result> result = listUsers.executeForBranch(BranchId.of("branch-1"), performedBy); + Result> result = listUsers.execute("branch-1", performedBy, pageRequest); assertThat(result.isSuccess()).isTrue(); - assertThat(result.unsafeGetValue()).hasSize(2); + assertThat(result.unsafeGetValue().content()).hasSize(2); } @Test @@ -104,10 +110,10 @@ class ListUsersTest { void should_FailWithUnauthorized_When_ActorLacksPermissionForBranchList() { when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(false); - Result> result = listUsers.executeForBranch(BranchId.of("branch-1"), performedBy); + Result> result = listUsers.execute("branch-1", performedBy, pageRequest); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class); - verify(userRepository, never()).findByBranchId(anyString()); + verify(userRepository, never()).findAll(any(), any()); } } diff --git a/backend/src/test/java/de/effigenix/domain/production/BatchTraceabilityServiceFuzzTest.java b/backend/src/test/java/de/effigenix/domain/production/BatchTraceabilityServiceFuzzTest.java index b58a14e..3ef8c62 100644 --- a/backend/src/test/java/de/effigenix/domain/production/BatchTraceabilityServiceFuzzTest.java +++ b/backend/src/test/java/de/effigenix/domain/production/BatchTraceabilityServiceFuzzTest.java @@ -2,10 +2,7 @@ package de.effigenix.domain.production; import com.code_intelligence.jazzer.api.FuzzedDataProvider; import com.code_intelligence.jazzer.junit.FuzzTest; -import de.effigenix.shared.common.Quantity; -import de.effigenix.shared.common.RepositoryError; -import de.effigenix.shared.common.Result; -import de.effigenix.shared.common.UnitOfMeasure; +import de.effigenix.shared.common.*; import java.math.BigDecimal; import java.time.LocalDate; @@ -237,6 +234,7 @@ class BatchTraceabilityServiceFuzzTest { @Override public Result> findByStatus(BatchStatus s) { throw new UnsupportedOperationException(); } @Override public Result> findByProductionDate(LocalDate d) { throw new UnsupportedOperationException(); } @Override public Result> findByRecipeIds(List ids) { throw new UnsupportedOperationException(); } + @Override public Result> findAllSummary(PageRequest pr) { throw new UnsupportedOperationException(); } @Override public Result> findAllSummary() { throw new UnsupportedOperationException(); } @Override public Result> findByStatusSummary(BatchStatus s) { throw new UnsupportedOperationException(); } @Override public Result> findByProductionDateSummary(LocalDate d) { throw new UnsupportedOperationException(); } diff --git a/backend/src/test/java/de/effigenix/infrastructure/inventory/web/InventoryCountControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/inventory/web/InventoryCountControllerIntegrationTest.java index eb43da6..14d5f13 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/inventory/web/InventoryCountControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/inventory/web/InventoryCountControllerIntegrationTest.java @@ -160,7 +160,7 @@ class InventoryCountControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/inventory/inventory-counts") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(1)); + .andExpect(jsonPath("$.content.length()").value(1)); } @Test @@ -175,11 +175,10 @@ class InventoryCountControllerIntegrationTest extends AbstractIntegrationTest { .andExpect(status().isCreated()); mockMvc.perform(get("/api/inventory/inventory-counts") - .param("storageLocationId", storageLocationId) .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(1)) - .andExpect(jsonPath("$[0].storageLocationId").value(storageLocationId)); + .andExpect(jsonPath("$.content.length()").value(1)) + .andExpect(jsonPath("$.content[0].storageLocationId").value(storageLocationId)); } // ==================== Security ==================== @@ -298,7 +297,7 @@ class InventoryCountControllerIntegrationTest extends AbstractIntegrationTest { .param("storageLocationId", UUID.randomUUID().toString()) .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(0)); + .andExpect(jsonPath("$.content.length()").value(0)); } // ==================== US-6.2: Inventur starten ==================== @@ -554,8 +553,8 @@ class InventoryCountControllerIntegrationTest extends AbstractIntegrationTest { .param("movementType", "ADJUSTMENT") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(org.hamcrest.Matchers.greaterThanOrEqualTo(1))) - .andExpect(jsonPath("$[0].movementType").value("ADJUSTMENT")); + .andExpect(jsonPath("$.content.length()").value(org.hamcrest.Matchers.greaterThanOrEqualTo(1))) + .andExpect(jsonPath("$.content[0].movementType").value("ADJUSTMENT")); } @Test @@ -783,8 +782,8 @@ class InventoryCountControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/inventory/inventory-counts?status=OPEN") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(1)) - .andExpect(jsonPath("$[0].status").value("OPEN")); + .andExpect(jsonPath("$.content.length()").value(1)) + .andExpect(jsonPath("$.content[0].status").value("OPEN")); } @Test diff --git a/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StockControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StockControllerIntegrationTest.java index a53efd8..e2447e0 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StockControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StockControllerIntegrationTest.java @@ -924,7 +924,7 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/inventory/stocks") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(2)); + .andExpect(jsonPath("$.content.length()").value(2)); } @Test @@ -938,8 +938,8 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest { .param("storageLocationId", storageLocationId) .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(1)) - .andExpect(jsonPath("$[0].storageLocationId").value(storageLocationId)); + .andExpect(jsonPath("$.content.length()").value(1)) + .andExpect(jsonPath("$.content[0].storageLocationId").value(storageLocationId)); } @Test @@ -952,8 +952,8 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest { .param("articleId", articleId) .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(1)) - .andExpect(jsonPath("$[0].articleId").value(articleId)); + .andExpect(jsonPath("$.content.length()").value(1)) + .andExpect(jsonPath("$.content[0].articleId").value(articleId)); } @Test @@ -962,7 +962,7 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/inventory/stocks") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(0)); + .andExpect(jsonPath("$.content.length()").value(0)); } @Test @@ -974,9 +974,9 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/inventory/stocks") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].batches.length()").value(1)) - .andExpect(jsonPath("$[0].totalQuantity").value(10)) - .andExpect(jsonPath("$[0].availableQuantity").value(10)); + .andExpect(jsonPath("$.content[0].batches.length()").value(1)) + .andExpect(jsonPath("$.content[0].totalQuantity").value(10)) + .andExpect(jsonPath("$.content[0].availableQuantity").value(10)); } @Test diff --git a/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StockMovementControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StockMovementControllerIntegrationTest.java index f80c177..77c94b2 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StockMovementControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StockMovementControllerIntegrationTest.java @@ -407,8 +407,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/inventory/stock-movements") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()) - .andExpect(jsonPath("$.length()").value(1)); + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content.length()").value(1)); } @Test @@ -420,7 +420,7 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest { .param("stockId", stockId) .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()); + .andExpect(jsonPath("$.content").isArray()); } @Test @@ -432,7 +432,7 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest { .param("movementType", "GOODS_RECEIPT") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()); + .andExpect(jsonPath("$.content").isArray()); } @Test @@ -444,8 +444,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest { .param("articleId", articleId) .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()) - .andExpect(jsonPath("$.length()").value(1)); + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content.length()").value(1)); } @Test @@ -472,8 +472,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/inventory/stock-movements") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()) - .andExpect(jsonPath("$.length()").value(0)); + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content.length()").value(0)); } @Test @@ -485,8 +485,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest { .param("batchReference", "CHARGE-001") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()) - .andExpect(jsonPath("$.length()").value(1)); + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content.length()").value(1)); } @Test @@ -499,8 +499,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest { .param("to", "2030-12-31T23:59:59Z") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()) - .andExpect(jsonPath("$.length()").value(1)); + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content.length()").value(1)); } @Test @@ -512,8 +512,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest { .param("from", "2020-01-01T00:00:00Z") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()) - .andExpect(jsonPath("$.length()").value(1)); + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content.length()").value(1)); } @Test @@ -525,8 +525,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest { .param("to", "2030-12-31T23:59:59Z") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()) - .andExpect(jsonPath("$.length()").value(1)); + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content.length()").value(1)); } @Test @@ -550,7 +550,7 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest { .param("batchReference", "CHARGE-001") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()); + .andExpect(jsonPath("$.content").isArray()); } @Test @@ -572,8 +572,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest { .param("batchReference", "UNKNOWN-CHARGE") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()) - .andExpect(jsonPath("$.length()").value(0)); + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content.length()").value(0)); } @Test @@ -586,8 +586,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest { .param("to", "2099-12-31T23:59:59Z") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()) - .andExpect(jsonPath("$.length()").value(0)); + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content.length()").value(0)); } } diff --git a/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StorageLocationControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StorageLocationControllerIntegrationTest.java index ac16cf6..23047cf 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StorageLocationControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StorageLocationControllerIntegrationTest.java @@ -500,11 +500,11 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/inventory/storage-locations") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(2))) - .andExpect(jsonPath("$[*].name", containsInAnyOrder("Lager A", "Lager B"))) - .andExpect(jsonPath("$[0].id").isNotEmpty()) - .andExpect(jsonPath("$[0].storageType").isNotEmpty()) - .andExpect(jsonPath("$[0].active").isBoolean()); + .andExpect(jsonPath("$.content", hasSize(2))) + .andExpect(jsonPath("$.content[*].name", containsInAnyOrder("Lager A", "Lager B"))) + .andExpect(jsonPath("$.content[0].id").isNotEmpty()) + .andExpect(jsonPath("$.content[0].storageType").isNotEmpty()) + .andExpect(jsonPath("$.content[0].active").isBoolean()); } @Test @@ -518,8 +518,8 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest { .param("storageType", "COLD_ROOM") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].storageType").value("COLD_ROOM")); + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].storageType").value("COLD_ROOM")); } @Test @@ -537,8 +537,8 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest { .param("active", "true") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].name").value("Aktiv")); + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].name").value("Aktiv")); } @Test @@ -555,8 +555,8 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest { .param("active", "false") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].name").value("Inaktiv")); + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].name").value("Inaktiv")); } @Test @@ -575,8 +575,8 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest { .param("active", "true") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].name").value("Kühl Aktiv")); + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].name").value("Kühl Aktiv")); } @Test @@ -595,8 +595,8 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest { .param("active", "false") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].name").value("Kühl Inaktiv")); + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].name").value("Kühl Inaktiv")); } @Test @@ -617,7 +617,7 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/inventory/storage-locations") .header("Authorization", "Bearer " + readerToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))); + .andExpect(jsonPath("$.content", hasSize(1))); } @Test @@ -641,7 +641,7 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/inventory/storage-locations") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(0))); + .andExpect(jsonPath("$.content", hasSize(0))); } @Test @@ -652,12 +652,12 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/inventory/storage-locations") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").isNotEmpty()) - .andExpect(jsonPath("$[0].name").value("Vollständig")) - .andExpect(jsonPath("$[0].storageType").value("COLD_ROOM")) - .andExpect(jsonPath("$[0].temperatureRange.minTemperature").value(-2)) - .andExpect(jsonPath("$[0].temperatureRange.maxTemperature").value(8)) - .andExpect(jsonPath("$[0].active").value(true)); + .andExpect(jsonPath("$.content[0].id").isNotEmpty()) + .andExpect(jsonPath("$.content[0].name").value("Vollständig")) + .andExpect(jsonPath("$.content[0].storageType").value("COLD_ROOM")) + .andExpect(jsonPath("$.content[0].temperatureRange.minTemperature").value(-2)) + .andExpect(jsonPath("$.content[0].temperatureRange.maxTemperature").value(8)) + .andExpect(jsonPath("$.content[0].active").value(true)); } // ==================== Hilfsmethoden ==================== diff --git a/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/ArticleControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/ArticleControllerIntegrationTest.java index 304ca55..530eae1 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/ArticleControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/ArticleControllerIntegrationTest.java @@ -181,7 +181,7 @@ class ArticleControllerIntegrationTest extends AbstractIntegrationTest { .param("status", "ACTIVE") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$[*].status", everyItem(is("ACTIVE")))); + .andExpect(jsonPath("$.content[*].status", everyItem(is("ACTIVE")))); } // ==================== TC-ART-07: Verkaufseinheit hinzufügen ==================== @@ -372,21 +372,19 @@ class ArticleControllerIntegrationTest extends AbstractIntegrationTest { .andExpect(jsonPath("$.supplierIds", hasSize(0))); } - // ==================== Artikel nach Kategorie filtern ==================== + // ==================== Artikel auflisten ==================== @Test - @DisplayName("Artikel nach categoryId filtern → nur passende zurückgegeben") - void filterByCategory_returnsOnlyMatching() throws Exception { - String otherCategoryId = createCategory("Milchprodukte"); + @DisplayName("Alle Artikel auflisten → alle vorhanden") + void listArticles_returnsAll() throws Exception { createArticle("Äpfel", "FI-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99"); + String otherCategoryId = createCategory("Milchprodukte"); createArticleInCategory("Milch", "MI-001", Unit.KG, PriceModel.WEIGHT_BASED, "1.29", otherCategoryId); mockMvc.perform(get("/api/articles") - .param("categoryId", otherCategoryId) .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].name").value("Milch")); + .andExpect(jsonPath("$.content", hasSize(2))); } // ==================== Artikel aktualisieren ==================== diff --git a/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/CustomerControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/CustomerControllerIntegrationTest.java index c998011..479025d 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/CustomerControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/CustomerControllerIntegrationTest.java @@ -181,45 +181,15 @@ class CustomerControllerIntegrationTest extends AbstractIntegrationTest { // ==================== TC-CUS-06: Filter ==================== @Test - @DisplayName("TC-CUS-06: Nur B2B-Kunden filtern → nur B2B") - void listCustomers_filterByB2B_returnsOnlyB2B() throws Exception { + @DisplayName("TC-CUS-06: Alle Kunden auflisten → verschiedene Typen enthalten") + void listCustomers_returnsAll() throws Exception { createB2bCustomer("Gastro GmbH"); createB2cCustomer("Max Mustermann"); mockMvc.perform(get("/api/customers") - .param("type", "B2B") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$[*].type", everyItem(is("B2B")))); - } - - @Test - @DisplayName("TC-CUS-06: Nur B2C-Kunden filtern → nur B2C") - void listCustomers_filterByB2C_returnsOnlyB2C() throws Exception { - createB2bCustomer("Gastro GmbH"); - createB2cCustomer("Max Mustermann"); - - mockMvc.perform(get("/api/customers") - .param("type", "B2C") - .header("Authorization", "Bearer " + adminToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[*].type", everyItem(is("B2C")))); - } - - @Test - @DisplayName("TC-CUS-06: Nur aktive Kunden filtern → nur ACTIVE") - void listCustomers_filterByActive_returnsOnlyActive() throws Exception { - String id = createB2cCustomer("Inaktiver Kunde"); - mockMvc.perform(post("/api/customers/{id}/deactivate", id) - .header("Authorization", "Bearer " + adminToken)).andReturn(); - - createB2cCustomer("Aktiver Kunde"); - - mockMvc.perform(get("/api/customers") - .param("status", "ACTIVE") - .header("Authorization", "Bearer " + adminToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[*].status", everyItem(is("ACTIVE")))); + .andExpect(jsonPath("$.content", hasSize(greaterThanOrEqualTo(2)))); } // ==================== TC-CUS-07: Lieferadresse hinzufügen ==================== diff --git a/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/ProductCategoryControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/ProductCategoryControllerIntegrationTest.java index be9bb6e..5a5167b 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/ProductCategoryControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/ProductCategoryControllerIntegrationTest.java @@ -122,7 +122,7 @@ class ProductCategoryControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/categories") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$[?(@.id == '" + categoryId + "')]").isEmpty()); + .andExpect(jsonPath("$.content[?(@.id == '" + categoryId + "')]").isEmpty()); } // ==================== TC-CAT-06: Leerer Name wird abgelehnt ==================== @@ -160,7 +160,7 @@ class ProductCategoryControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/categories") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(2)))); + .andExpect(jsonPath("$.content", hasSize(greaterThanOrEqualTo(2)))); } // ==================== TC-AUTH: Autorisierung ==================== diff --git a/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/SupplierControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/SupplierControllerIntegrationTest.java index 9529573..face236 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/SupplierControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/masterdata/web/SupplierControllerIntegrationTest.java @@ -173,7 +173,7 @@ class SupplierControllerIntegrationTest extends AbstractIntegrationTest { .param("status", "ACTIVE") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$[*].status", everyItem(is("ACTIVE")))); + .andExpect(jsonPath("$.content[*].status", everyItem(is("ACTIVE")))); } @Test @@ -187,7 +187,7 @@ class SupplierControllerIntegrationTest extends AbstractIntegrationTest { .param("status", "INACTIVE") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$[*].status", everyItem(is("INACTIVE")))); + .andExpect(jsonPath("$.content[*].status", everyItem(is("INACTIVE")))); } // ==================== TC-SUP-07: Lieferant bewerten ==================== diff --git a/backend/src/test/java/de/effigenix/infrastructure/production/web/ListBatchesIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/production/web/ListBatchesIntegrationTest.java index 025e776..9450ce7 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/production/web/ListBatchesIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/production/web/ListBatchesIntegrationTest.java @@ -42,10 +42,10 @@ class ListBatchesIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/production/batches") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(2))) - .andExpect(jsonPath("$[0].id").isNotEmpty()) - .andExpect(jsonPath("$[0].batchNumber").isNotEmpty()) - .andExpect(jsonPath("$[0].status").value("PLANNED")); + .andExpect(jsonPath("$.content", hasSize(2))) + .andExpect(jsonPath("$.content[0].id").isNotEmpty()) + .andExpect(jsonPath("$.content[0].batchNumber").isNotEmpty()) + .andExpect(jsonPath("$.content[0].status").value("PLANNED")); } @Test @@ -54,7 +54,7 @@ class ListBatchesIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/production/batches") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(0))); + .andExpect(jsonPath("$.content", hasSize(0))); } @Test @@ -66,8 +66,8 @@ class ListBatchesIntegrationTest extends AbstractIntegrationTest { .param("status", "PLANNED") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].status").value("PLANNED")); + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].status").value("PLANNED")); } @Test @@ -90,8 +90,8 @@ class ListBatchesIntegrationTest extends AbstractIntegrationTest { .param("productionDate", "2026-03-01") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].productionDate").value("2026-03-01")); + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].productionDate").value("2026-03-01")); } @Test @@ -103,7 +103,7 @@ class ListBatchesIntegrationTest extends AbstractIntegrationTest { .param("articleId", "article-123") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))); + .andExpect(jsonPath("$.content", hasSize(1))); } @Test @@ -113,7 +113,7 @@ class ListBatchesIntegrationTest extends AbstractIntegrationTest { .param("articleId", "unknown-article") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(0))); + .andExpect(jsonPath("$.content", hasSize(0))); } @Test diff --git a/backend/src/test/java/de/effigenix/infrastructure/production/web/ListRecipesIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/production/web/ListRecipesIntegrationTest.java index 9806256..83e4466 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/production/web/ListRecipesIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/production/web/ListRecipesIntegrationTest.java @@ -41,11 +41,11 @@ class ListRecipesIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/recipes") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(2))) - .andExpect(jsonPath("$[0].ingredientCount").isNumber()) - .andExpect(jsonPath("$[0].stepCount").isNumber()) - .andExpect(jsonPath("$[0].ingredients").doesNotExist()) - .andExpect(jsonPath("$[0].productionSteps").doesNotExist()); + .andExpect(jsonPath("$.content", hasSize(2))) + .andExpect(jsonPath("$.content[0].ingredientCount").isNumber()) + .andExpect(jsonPath("$.content[0].stepCount").isNumber()) + .andExpect(jsonPath("$.content[0].ingredients").doesNotExist()) + .andExpect(jsonPath("$.content[0].productionSteps").doesNotExist()); } @Test @@ -62,9 +62,9 @@ class ListRecipesIntegrationTest extends AbstractIntegrationTest { .param("status", "ACTIVE") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].name").value("Bratwurst")) - .andExpect(jsonPath("$[0].status").value("ACTIVE")); + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].name").value("Bratwurst")) + .andExpect(jsonPath("$.content[0].status").value("ACTIVE")); } @Test @@ -83,7 +83,7 @@ class ListRecipesIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/recipes") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(0))); + .andExpect(jsonPath("$.content", hasSize(0))); } @Test diff --git a/backend/src/test/java/de/effigenix/infrastructure/production/web/ProductionOrderControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/production/web/ProductionOrderControllerIntegrationTest.java index ac129f6..0adfa2a 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/production/web/ProductionOrderControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/production/web/ProductionOrderControllerIntegrationTest.java @@ -915,7 +915,7 @@ class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/production/production-orders") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(org.hamcrest.Matchers.greaterThanOrEqualTo(2))); + .andExpect(jsonPath("$.content.length()").value(org.hamcrest.Matchers.greaterThanOrEqualTo(2))); } @Test @@ -928,7 +928,7 @@ class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest { .param("status", "PLANNED") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].status").value("PLANNED")); + .andExpect(jsonPath("$.content[0].status").value("PLANNED")); } @Test @@ -941,7 +941,7 @@ class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest { .param("dateTo", LocalDate.now().plusDays(30).toString()) .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(org.hamcrest.Matchers.greaterThanOrEqualTo(1))); + .andExpect(jsonPath("$.content.length()").value(org.hamcrest.Matchers.greaterThanOrEqualTo(1))); } @Test @@ -955,7 +955,7 @@ class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest { .param("status", "PLANNED") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].status").value("PLANNED")); + .andExpect(jsonPath("$.content[0].status").value("PLANNED")); } @Test @@ -968,7 +968,7 @@ class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest { .param("dateTo", LocalDate.now().plusYears(11).toString()) .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(0)); + .andExpect(jsonPath("$.content.length()").value(0)); } @Test diff --git a/backend/src/test/java/de/effigenix/infrastructure/shared/persistence/PaginationHelperTest.java b/backend/src/test/java/de/effigenix/infrastructure/shared/persistence/PaginationHelperTest.java new file mode 100644 index 0000000..b328f49 --- /dev/null +++ b/backend/src/test/java/de/effigenix/infrastructure/shared/persistence/PaginationHelperTest.java @@ -0,0 +1,63 @@ +package de.effigenix.infrastructure.shared.persistence; + +import de.effigenix.shared.common.SortField; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("PaginationHelper") +class PaginationHelperTest { + + private static final Map ALLOWED = Map.of( + "name", "name", + "createdAt", "created_at", + "status", "status" + ); + + @Test + @DisplayName("should return empty string for empty sort list") + void emptySort() { + assertThat(PaginationHelper.buildOrderByClause(List.of(), ALLOWED)).isEmpty(); + } + + @Test + @DisplayName("should return empty string for null sort list") + void nullSort() { + assertThat(PaginationHelper.buildOrderByClause(null, ALLOWED)).isEmpty(); + } + + @Test + @DisplayName("should build single field ORDER BY") + void singleField() { + var sort = List.of(SortField.desc("createdAt")); + assertThat(PaginationHelper.buildOrderByClause(sort, ALLOWED)) + .isEqualTo(" ORDER BY created_at DESC"); + } + + @Test + @DisplayName("should build multi-field ORDER BY") + void multiField() { + var sort = List.of(SortField.asc("name"), SortField.desc("createdAt")); + assertThat(PaginationHelper.buildOrderByClause(sort, ALLOWED)) + .isEqualTo(" ORDER BY name ASC, created_at DESC"); + } + + @Test + @DisplayName("should ignore unknown fields") + void unknownFields() { + var sort = List.of(SortField.asc("unknown"), SortField.desc("createdAt")); + assertThat(PaginationHelper.buildOrderByClause(sort, ALLOWED)) + .isEqualTo(" ORDER BY created_at DESC"); + } + + @Test + @DisplayName("should return empty when all fields unknown") + void allUnknown() { + var sort = List.of(SortField.asc("unknown")); + assertThat(PaginationHelper.buildOrderByClause(sort, ALLOWED)).isEmpty(); + } +} diff --git a/backend/src/test/java/de/effigenix/infrastructure/shared/web/CountryControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/shared/web/CountryControllerIntegrationTest.java index 10498de..6623443 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/shared/web/CountryControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/shared/web/CountryControllerIntegrationTest.java @@ -15,12 +15,13 @@ class CountryControllerIntegrationTest extends AbstractIntegrationTest { @Test void shouldReturnCountriesWithDachFirst() throws Exception { mockMvc.perform(get("/api/countries") + .param("size", "100") .header("Authorization", "Bearer " + validToken())) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(greaterThan(200)))) - .andExpect(jsonPath("$[0].code").value("DE")) - .andExpect(jsonPath("$[1].code").value("AT")) - .andExpect(jsonPath("$[2].code").value("CH")); + .andExpect(jsonPath("$.page.totalElements", greaterThan(200))) + .andExpect(jsonPath("$.content[0].code").value("DE")) + .andExpect(jsonPath("$.content[1].code").value("AT")) + .andExpect(jsonPath("$.content[2].code").value("CH")); } @Test diff --git a/backend/src/test/java/de/effigenix/infrastructure/usermanagement/web/RoleControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/usermanagement/web/RoleControllerIntegrationTest.java index 42c9fcb..cb680bc 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/usermanagement/web/RoleControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/usermanagement/web/RoleControllerIntegrationTest.java @@ -37,7 +37,7 @@ class RoleControllerIntegrationTest extends AbstractIntegrationTest { mockMvc.perform(get("/api/roles") .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(2)))); + .andExpect(jsonPath("$.content", hasSize(greaterThanOrEqualTo(2)))); } @Test diff --git a/backend/src/test/java/de/effigenix/infrastructure/usermanagement/web/UserControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/usermanagement/web/UserControllerIntegrationTest.java index 0e72628..db90b0e 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/usermanagement/web/UserControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/usermanagement/web/UserControllerIntegrationTest.java @@ -207,10 +207,10 @@ class UserControllerIntegrationTest extends AbstractIntegrationTest { .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(2)))) - .andExpect(jsonPath("$[0].id").isNotEmpty()) - .andExpect(jsonPath("$[0].username").isNotEmpty()) - .andExpect(jsonPath("$[0].email").isNotEmpty()) + .andExpect(jsonPath("$.content", hasSize(greaterThanOrEqualTo(2)))) + .andExpect(jsonPath("$.content[0].id").isNotEmpty()) + .andExpect(jsonPath("$.content[0].username").isNotEmpty()) + .andExpect(jsonPath("$.content[0].email").isNotEmpty()) .andReturn(); } diff --git a/backend/src/test/java/de/effigenix/shared/common/PageRequestTest.java b/backend/src/test/java/de/effigenix/shared/common/PageRequestTest.java new file mode 100644 index 0000000..8b3f101 --- /dev/null +++ b/backend/src/test/java/de/effigenix/shared/common/PageRequestTest.java @@ -0,0 +1,84 @@ +package de.effigenix.shared.common; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("PageRequest") +class PageRequestTest { + + @Nested + @DisplayName("of()") + class Of { + + @Test + @DisplayName("should create valid page request") + void valid() { + var pr = PageRequest.of(2, 20); + assertThat(pr.page()).isEqualTo(2); + assertThat(pr.size()).isEqualTo(20); + assertThat(pr.sort()).isEmpty(); + } + + @Test + @DisplayName("should create with sort fields") + void withSort() { + var sort = List.of(SortField.asc("name"), SortField.desc("createdAt")); + var pr = PageRequest.of(0, 10, sort); + assertThat(pr.sort()).hasSize(2); + } + + @Test + @DisplayName("should accept page 0") + void pageZero() { + var pr = PageRequest.of(0, 20); + assertThat(pr.page()).isEqualTo(0); + } + + @Test + @DisplayName("should reject negative page") + void negativePage() { + assertThatThrownBy(() -> PageRequest.of(-1, 20)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("should reject size 0") + void sizeZero() { + assertThatThrownBy(() -> PageRequest.of(0, 0)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("should reject size > 100") + void sizeTooBig() { + assertThatThrownBy(() -> PageRequest.of(0, 101)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("should accept size 100") + void sizeMax() { + var pr = PageRequest.of(0, 100); + assertThat(pr.size()).isEqualTo(100); + } + } + + @Nested + @DisplayName("offset()") + class Offset { + + @Test + @DisplayName("should calculate correct offset") + void calculatesOffset() { + assertThat(PageRequest.of(0, 20).offset()).isEqualTo(0); + assertThat(PageRequest.of(1, 20).offset()).isEqualTo(20); + assertThat(PageRequest.of(3, 10).offset()).isEqualTo(30); + } + } +} diff --git a/backend/src/test/java/de/effigenix/shared/common/PageTest.java b/backend/src/test/java/de/effigenix/shared/common/PageTest.java new file mode 100644 index 0000000..da86088 --- /dev/null +++ b/backend/src/test/java/de/effigenix/shared/common/PageTest.java @@ -0,0 +1,73 @@ +package de.effigenix.shared.common; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Page") +class PageTest { + + @Nested + @DisplayName("of()") + class Of { + + @Test + @DisplayName("should create page with calculated totalPages") + void calculatesTotalPages() { + var page = Page.of(List.of("a", "b"), 0, 2, 5); + assertThat(page.content()).containsExactly("a", "b"); + assertThat(page.number()).isEqualTo(0); + assertThat(page.size()).isEqualTo(2); + assertThat(page.totalElements()).isEqualTo(5); + assertThat(page.totalPages()).isEqualTo(3); + } + + @Test + @DisplayName("should handle exact division") + void exactDivision() { + var page = Page.of(List.of("a", "b"), 0, 2, 4); + assertThat(page.totalPages()).isEqualTo(2); + } + + @Test + @DisplayName("should handle single element") + void singleElement() { + var page = Page.of(List.of("a"), 0, 20, 1); + assertThat(page.totalPages()).isEqualTo(1); + } + } + + @Nested + @DisplayName("empty()") + class Empty { + + @Test + @DisplayName("should create empty page") + void emptyPage() { + var page = Page.empty(20); + assertThat(page.content()).isEmpty(); + assertThat(page.totalElements()).isEqualTo(0); + assertThat(page.totalPages()).isEqualTo(0); + } + } + + @Nested + @DisplayName("map()") + class Map { + + @Test + @DisplayName("should map content while preserving pagination info") + void mapsContent() { + var page = Page.of(List.of(1, 2, 3), 0, 10, 3); + var mapped = page.map(String::valueOf); + assertThat(mapped.content()).containsExactly("1", "2", "3"); + assertThat(mapped.number()).isEqualTo(0); + assertThat(mapped.size()).isEqualTo(10); + assertThat(mapped.totalElements()).isEqualTo(3); + } + } +} diff --git a/backend/src/test/java/de/effigenix/shared/common/SortFieldTest.java b/backend/src/test/java/de/effigenix/shared/common/SortFieldTest.java new file mode 100644 index 0000000..00aa574 --- /dev/null +++ b/backend/src/test/java/de/effigenix/shared/common/SortFieldTest.java @@ -0,0 +1,83 @@ +package de.effigenix.shared.common; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("SortField") +class SortFieldTest { + + @Nested + @DisplayName("parse()") + class Parse { + + @Test + @DisplayName("should parse field with desc direction") + void fieldWithDesc() { + var sf = SortField.parse("createdAt,desc"); + assertThat(sf.field()).isEqualTo("createdAt"); + assertThat(sf.direction()).isEqualTo(SortDirection.DESC); + } + + @Test + @DisplayName("should parse field with asc direction") + void fieldWithAsc() { + var sf = SortField.parse("name,asc"); + assertThat(sf.field()).isEqualTo("name"); + assertThat(sf.direction()).isEqualTo(SortDirection.ASC); + } + + @Test + @DisplayName("should default to ASC when no direction given") + void fieldOnly() { + var sf = SortField.parse("name"); + assertThat(sf.field()).isEqualTo("name"); + assertThat(sf.direction()).isEqualTo(SortDirection.ASC); + } + + @Test + @DisplayName("should be case-insensitive for direction") + void caseInsensitive() { + var sf = SortField.parse("name,DESC"); + assertThat(sf.direction()).isEqualTo(SortDirection.DESC); + } + + @Test + @DisplayName("should throw on blank input") + void blankInput() { + assertThatThrownBy(() -> SortField.parse("")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("should throw on null input") + void nullInput() { + assertThatThrownBy(() -> SortField.parse(null)) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + @DisplayName("factories") + class Factories { + + @Test + @DisplayName("asc() should create ascending sort field") + void ascFactory() { + var sf = SortField.asc("name"); + assertThat(sf.field()).isEqualTo("name"); + assertThat(sf.direction()).isEqualTo(SortDirection.ASC); + } + + @Test + @DisplayName("desc() should create descending sort field") + void descFactory() { + var sf = SortField.desc("createdAt"); + assertThat(sf.field()).isEqualTo("createdAt"); + assertThat(sf.direction()).isEqualTo(SortDirection.DESC); + } + } +} diff --git a/frontend/apps/cli/src/components/inventory/InventoryCountListScreen.tsx b/frontend/apps/cli/src/components/inventory/InventoryCountListScreen.tsx index 48fd30e..ad3258d 100644 --- a/frontend/apps/cli/src/components/inventory/InventoryCountListScreen.tsx +++ b/frontend/apps/cli/src/components/inventory/InventoryCountListScreen.tsx @@ -20,13 +20,13 @@ const STATUS_COLORS: Record = { export function InventoryCountListScreen() { const { navigate, back } = useNavigation(); - const { inventoryCounts, loading, error, fetchInventoryCounts, clearError } = useInventoryCounts(); + const { inventoryCounts, loading, error, fetchInventoryCounts, clearError, currentPage, totalElements, totalPages, pageSize } = useInventoryCounts(); const { locationName } = useStockNameLookup(); const [selectedIndex, setSelectedIndex] = useState(0); const [statusFilter, setStatusFilter] = useState('ALL'); useEffect(() => { - void fetchInventoryCounts(); + void fetchInventoryCounts(undefined, { page: 0, size: 20 }); }, [fetchInventoryCounts]); const filtered = statusFilter === 'ALL' @@ -44,7 +44,15 @@ export function InventoryCountListScreen() { if (item) navigate('inventory-count-detail', { inventoryCountId: item.id }); } if (input === 'n') navigate('inventory-count-create'); - if (input === 'r') void fetchInventoryCounts(); + if (input === 'r') void fetchInventoryCounts(undefined, { page: currentPage, size: pageSize }); + if (key.leftArrow && currentPage > 0) { + void fetchInventoryCounts(undefined, { page: currentPage - 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.rightArrow && currentPage < totalPages - 1) { + void fetchInventoryCounts(undefined, { page: currentPage + 1, size: pageSize }); + setSelectedIndex(0); + } if (input === 'f') { setStatusFilter((current) => { const idx = FILTER_CYCLE.indexOf(current); @@ -108,6 +116,11 @@ export function InventoryCountListScreen() { + {totalPages > 1 + ? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · ` + : totalElements > 0 + ? `${totalElements} Einträge · ` + : ''} ↑↓ nav · Enter Details · [n] Neu · [f] Filter · [r] Refresh · Backspace Zurück diff --git a/frontend/apps/cli/src/components/inventory/StockListScreen.tsx b/frontend/apps/cli/src/components/inventory/StockListScreen.tsx index e003a18..6d9d4d3 100644 --- a/frontend/apps/cli/src/components/inventory/StockListScreen.tsx +++ b/frontend/apps/cli/src/components/inventory/StockListScreen.tsx @@ -8,14 +8,14 @@ import { ErrorDisplay } from '../shared/ErrorDisplay.js'; export function StockListScreen() { const { navigate, back } = useNavigation(); - const { stocks, loading, error, fetchStocks, clearError } = useStocks(); + const { stocks, loading, error, fetchStocks, clearError, currentPage, totalElements, totalPages, pageSize } = useStocks(); const { articleName, locationName } = useStockNameLookup(); const [selectedIndex, setSelectedIndex] = useState(0); const [searchMode, setSearchMode] = useState(false); const [searchQuery, setSearchQuery] = useState(''); useEffect(() => { - void fetchStocks(); + void fetchStocks(undefined, { page: 0, size: 20 }); }, [fetchStocks]); const filtered = React.useMemo(() => { @@ -73,8 +73,16 @@ export function StockListScreen() { const stock = filtered[selectedIndex]; if (stock) navigate('stock-detail', { stockId: stock.id }); } + if (key.leftArrow && currentPage > 0) { + void fetchStocks(undefined, { page: currentPage - 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.rightArrow && currentPage < totalPages - 1) { + void fetchStocks(undefined, { page: currentPage + 1, size: pageSize }); + setSelectedIndex(0); + } if (input === 'n') navigate('stock-create'); - if (input === 'r') void fetchStocks(); + if (input === 'r') void fetchStocks(undefined, { page: currentPage, size: pageSize }); if (input === 's' || input === '/') { setSearchMode(true); setSelectedIndex(0); @@ -136,6 +144,11 @@ export function StockListScreen() { + {totalPages > 1 + ? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · ` + : totalElements > 0 + ? `${totalElements} Einträge · ` + : ''} ↑↓ nav · Enter Details · [n] Neu · [r] Aktualisieren · [s] Suche · Backspace Zurück diff --git a/frontend/apps/cli/src/components/inventory/StockMovementListScreen.tsx b/frontend/apps/cli/src/components/inventory/StockMovementListScreen.tsx index a90a979..1bd382e 100644 --- a/frontend/apps/cli/src/components/inventory/StockMovementListScreen.tsx +++ b/frontend/apps/cli/src/components/inventory/StockMovementListScreen.tsx @@ -26,7 +26,7 @@ const TYPE_FILTER_OPTIONS: { key: string; label: string; value: string | undefin export function StockMovementListScreen() { const { navigate, back, params } = useNavigation(); - const { movements, loading, error, fetchMovements, clearError } = useStockMovements(); + const { movements, loading, error, fetchMovements, clearError, currentPage, totalElements, totalPages, pageSize } = useStockMovements(); const [selectedIndex, setSelectedIndex] = useState(0); const [typeFilter, setTypeFilter] = useState(undefined); @@ -36,7 +36,7 @@ export function StockMovementListScreen() { const filter: StockMovementFilter = {}; if (stockId) filter.stockId = stockId; if (typeFilter) filter.movementType = typeFilter; - void fetchMovements(filter); + void fetchMovements(filter, { page: 0, size: 20 }); }, [fetchMovements, stockId, typeFilter]); useInput((input, key) => { @@ -54,7 +54,21 @@ export function StockMovementListScreen() { const filter: StockMovementFilter = {}; if (stockId) filter.stockId = stockId; if (typeFilter) filter.movementType = typeFilter; - void fetchMovements(filter); + void fetchMovements(filter, { page: currentPage, size: pageSize }); + } + if (key.leftArrow && currentPage > 0) { + const filter: StockMovementFilter = {}; + if (stockId) filter.stockId = stockId; + if (typeFilter) filter.movementType = typeFilter; + void fetchMovements(filter, { page: currentPage - 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.rightArrow && currentPage < totalPages - 1) { + const filter: StockMovementFilter = {}; + if (stockId) filter.stockId = stockId; + if (typeFilter) filter.movementType = typeFilter; + void fetchMovements(filter, { page: currentPage + 1, size: pageSize }); + setSelectedIndex(0); } if (key.backspace || key.escape) back(); @@ -119,6 +133,11 @@ export function StockMovementListScreen() { + {totalPages > 1 + ? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · ` + : totalElements > 0 + ? `${totalElements} Einträge · ` + : ''} ↑↓ nav · Enter Details · [n] Neu · [r] Aktualisieren · [a] Alle [1-8] Typ-Filter · Backspace Zurück diff --git a/frontend/apps/cli/src/components/inventory/StorageLocationListScreen.tsx b/frontend/apps/cli/src/components/inventory/StorageLocationListScreen.tsx index 0890434..f97228a 100644 --- a/frontend/apps/cli/src/components/inventory/StorageLocationListScreen.tsx +++ b/frontend/apps/cli/src/components/inventory/StorageLocationListScreen.tsx @@ -13,7 +13,7 @@ const STORAGE_TYPES: StorageType[] = ['COLD_ROOM', 'FREEZER', 'DRY_STORAGE', 'DI export function StorageLocationListScreen() { const { navigate, back } = useNavigation(); - const { storageLocations, loading, error, fetchStorageLocations, clearError } = useStorageLocations(); + const { storageLocations, loading, error, fetchStorageLocations, clearError, currentPage, totalElements, totalPages, pageSize } = useStorageLocations(); const [selectedIndex, setSelectedIndex] = useState(0); const [statusFilter, setStatusFilter] = useState('ALL'); const [typeFilter, setTypeFilter] = useState(null); @@ -22,7 +22,7 @@ export function StorageLocationListScreen() { const filter: StorageLocationFilter = {}; if (typeFilter) filter.storageType = typeFilter; if (statusFilter !== 'ALL') filter.active = statusFilter === 'ACTIVE'; - void fetchStorageLocations(filter); + void fetchStorageLocations(filter, { page: 0, size: 20 }); }, [fetchStorageLocations, statusFilter, typeFilter]); useInput((input, key) => { @@ -48,6 +48,20 @@ export function StorageLocationListScreen() { }); setSelectedIndex(0); } + if (key.leftArrow && currentPage > 0) { + const filter: StorageLocationFilter = {}; + if (typeFilter) filter.storageType = typeFilter; + if (statusFilter !== 'ALL') filter.active = statusFilter === 'ACTIVE'; + void fetchStorageLocations(filter, { page: currentPage - 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.rightArrow && currentPage < totalPages - 1) { + const filter: StorageLocationFilter = {}; + if (typeFilter) filter.storageType = typeFilter; + if (statusFilter !== 'ALL') filter.active = statusFilter === 'ACTIVE'; + void fetchStorageLocations(filter, { page: currentPage + 1, size: pageSize }); + setSelectedIndex(0); + } if (key.backspace || key.escape) back(); }); @@ -103,6 +117,11 @@ export function StorageLocationListScreen() { + {totalPages > 1 + ? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · ` + : totalElements > 0 + ? `${totalElements} Einträge · ` + : ''} ↑↓ nav · Enter Details · [n] Neu · [a] Alle · [A] Aktiv · [I] Inaktiv · [t] Typ · Backspace Zurück diff --git a/frontend/apps/cli/src/components/masterdata/articles/ArticleListScreen.tsx b/frontend/apps/cli/src/components/masterdata/articles/ArticleListScreen.tsx index 1efc16c..1777d5b 100644 --- a/frontend/apps/cli/src/components/masterdata/articles/ArticleListScreen.tsx +++ b/frontend/apps/cli/src/components/masterdata/articles/ArticleListScreen.tsx @@ -10,12 +10,12 @@ type Filter = 'ALL' | ArticleStatus; export function ArticleListScreen() { const { navigate, back } = useNavigation(); - const { articles, loading, error, fetchArticles, clearError } = useArticles(); + const { articles, loading, error, fetchArticles, clearError, currentPage, totalElements, totalPages, pageSize } = useArticles(); const [selectedIndex, setSelectedIndex] = useState(0); const [filter, setFilter] = useState('ALL'); useEffect(() => { - void fetchArticles(); + void fetchArticles({ page: 0, size: 20 }); }, [fetchArticles]); const filtered = filter === 'ALL' ? articles : articles.filter((a) => a.status === filter); @@ -32,6 +32,14 @@ export function ArticleListScreen() { if (input === 'a') { setFilter('ALL'); setSelectedIndex(0); } if (input === 'A') { setFilter('ACTIVE'); setSelectedIndex(0); } if (input === 'I') { setFilter('INACTIVE'); setSelectedIndex(0); } + if (key.leftArrow && currentPage > 0) { + void fetchArticles({ page: currentPage - 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.rightArrow && currentPage < totalPages - 1) { + void fetchArticles({ page: currentPage + 1, size: pageSize }); + setSelectedIndex(0); + } if (key.backspace || key.escape) back(); }); @@ -77,6 +85,11 @@ export function ArticleListScreen() { + {totalPages > 1 + ? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · ` + : totalElements > 0 + ? `${totalElements} Einträge · ` + : ''} ↑↓ nav · Enter Details · [n] Neu · [a] Alle · [A] Aktiv · [I] Inaktiv · Backspace Zurück diff --git a/frontend/apps/cli/src/components/masterdata/categories/CategoryListScreen.tsx b/frontend/apps/cli/src/components/masterdata/categories/CategoryListScreen.tsx index 2e2aaef..88fbef0 100644 --- a/frontend/apps/cli/src/components/masterdata/categories/CategoryListScreen.tsx +++ b/frontend/apps/cli/src/components/masterdata/categories/CategoryListScreen.tsx @@ -9,13 +9,13 @@ import { SuccessDisplay } from '../../shared/SuccessDisplay.js'; export function CategoryListScreen() { const { navigate, back } = useNavigation(); - const { categories, loading, error, fetchCategories, deleteCategory, clearError } = useCategories(); + const { categories, loading, error, fetchCategories, deleteCategory, clearError, currentPage, totalElements, totalPages, pageSize } = useCategories(); const [selectedIndex, setSelectedIndex] = useState(0); const [confirmDeleteId, setConfirmDeleteId] = useState(null); const [successMessage, setSuccessMessage] = useState(null); useEffect(() => { - void fetchCategories(); + void fetchCategories({ page: 0, size: 20 }); }, [fetchCategories]); useInput((input, key) => { @@ -33,6 +33,14 @@ export function CategoryListScreen() { const cat = categories[selectedIndex]; if (cat) setConfirmDeleteId(cat.id); } + if (key.leftArrow && currentPage > 0) { + void fetchCategories({ page: currentPage - 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.rightArrow && currentPage < totalPages - 1) { + void fetchCategories({ page: currentPage + 1, size: pageSize }); + setSelectedIndex(0); + } if (key.backspace || key.escape) back(); }); @@ -93,6 +101,11 @@ export function CategoryListScreen() { + {totalPages > 1 + ? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · ` + : totalElements > 0 + ? `${totalElements} Einträge · ` + : ''} ↑↓ navigieren · Enter Details · [n] Neu · [d] Löschen · Backspace Zurück diff --git a/frontend/apps/cli/src/components/masterdata/customers/CustomerListScreen.tsx b/frontend/apps/cli/src/components/masterdata/customers/CustomerListScreen.tsx index 92ff9ed..12b6ea9 100644 --- a/frontend/apps/cli/src/components/masterdata/customers/CustomerListScreen.tsx +++ b/frontend/apps/cli/src/components/masterdata/customers/CustomerListScreen.tsx @@ -11,13 +11,13 @@ type TypeFilter = 'ALL' | CustomerType; export function CustomerListScreen() { const { navigate, back } = useNavigation(); - const { customers, loading, error, fetchCustomers, clearError } = useCustomers(); + const { customers, loading, error, fetchCustomers, clearError, currentPage, totalElements, totalPages, pageSize } = useCustomers(); const [selectedIndex, setSelectedIndex] = useState(0); const [statusFilter, setStatusFilter] = useState('ALL'); const [typeFilter, setTypeFilter] = useState('ALL'); useEffect(() => { - void fetchCustomers(); + void fetchCustomers({ page: 0, size: 20 }); }, [fetchCustomers]); const filtered = customers.filter( @@ -41,6 +41,14 @@ export function CustomerListScreen() { if (input === 'b') { setTypeFilter('ALL'); setSelectedIndex(0); } if (input === 'B') { setTypeFilter('B2B'); setSelectedIndex(0); } if (input === 'C') { setTypeFilter('B2C'); setSelectedIndex(0); } + if (key.leftArrow && currentPage > 0) { + void fetchCustomers({ page: currentPage - 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.rightArrow && currentPage < totalPages - 1) { + void fetchCustomers({ page: currentPage + 1, size: pageSize }); + setSelectedIndex(0); + } if (key.backspace || key.escape) back(); }); @@ -88,6 +96,11 @@ export function CustomerListScreen() { + {totalPages > 1 + ? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · ` + : totalElements > 0 + ? `${totalElements} Einträge · ` + : ''} ↑↓ nav · Enter Details · [n] Neu · [a/A/I] Status · [b/B/C] Typ · Backspace Zurück diff --git a/frontend/apps/cli/src/components/masterdata/suppliers/SupplierListScreen.tsx b/frontend/apps/cli/src/components/masterdata/suppliers/SupplierListScreen.tsx index 383a5ce..ca86e9e 100644 --- a/frontend/apps/cli/src/components/masterdata/suppliers/SupplierListScreen.tsx +++ b/frontend/apps/cli/src/components/masterdata/suppliers/SupplierListScreen.tsx @@ -16,12 +16,12 @@ function avgRating(rating: { qualityScore: number; deliveryScore: number; priceS export function SupplierListScreen() { const { navigate, back } = useNavigation(); - const { suppliers, loading, error, fetchSuppliers, clearError } = useSuppliers(); + const { suppliers, loading, error, fetchSuppliers, clearError, currentPage, totalElements, totalPages, pageSize } = useSuppliers(); const [selectedIndex, setSelectedIndex] = useState(0); const [filter, setFilter] = useState('ALL'); useEffect(() => { - void fetchSuppliers(); + void fetchSuppliers({ page: 0, size: 20 }); }, [fetchSuppliers]); const filtered = filter === 'ALL' ? suppliers : suppliers.filter((s) => s.status === filter); @@ -40,6 +40,14 @@ export function SupplierListScreen() { if (input === 'a') { setFilter('ALL'); setSelectedIndex(0); } if (input === 'A') { setFilter('ACTIVE'); setSelectedIndex(0); } if (input === 'I') { setFilter('INACTIVE'); setSelectedIndex(0); } + if (key.leftArrow && currentPage > 0) { + void fetchSuppliers({ page: currentPage - 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.rightArrow && currentPage < totalPages - 1) { + void fetchSuppliers({ page: currentPage + 1, size: pageSize }); + setSelectedIndex(0); + } if (key.backspace || key.escape) back(); }); @@ -85,6 +93,11 @@ export function SupplierListScreen() { + {totalPages > 1 + ? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · ` + : totalElements > 0 + ? `${totalElements} Einträge · ` + : ''} ↑↓ nav · Enter Details · [n] Neu · [a] Alle · [A] Aktiv · [I] Inaktiv · Backspace Zurück diff --git a/frontend/apps/cli/src/components/production/BatchListScreen.tsx b/frontend/apps/cli/src/components/production/BatchListScreen.tsx index 921ad2f..623949f 100644 --- a/frontend/apps/cli/src/components/production/BatchListScreen.tsx +++ b/frontend/apps/cli/src/components/production/BatchListScreen.tsx @@ -24,12 +24,12 @@ const STATUS_COLORS: Record = { export function BatchListScreen() { const { navigate, back } = useNavigation(); - const { batches, loading, error, fetchBatches, clearError } = useBatches(); + const { batches, loading, error, fetchBatches, clearError, currentPage, totalElements, totalPages, pageSize } = useBatches(); const [selectedIndex, setSelectedIndex] = useState(0); const [statusFilter, setStatusFilter] = useState(undefined); useEffect(() => { - void fetchBatches(statusFilter); + void fetchBatches(statusFilter, { page: 0, size: 20 }); }, [fetchBatches, statusFilter]); useInput((input, key) => { @@ -43,6 +43,14 @@ export function BatchListScreen() { if (batch?.id) navigate('batch-detail', { batchId: batch.id }); } if (input === 'n') navigate('batch-plan'); + if (key.leftArrow && currentPage > 0) { + void fetchBatches(statusFilter, { page: currentPage - 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.rightArrow && currentPage < totalPages - 1) { + void fetchBatches(statusFilter, { page: currentPage + 1, size: pageSize }); + setSelectedIndex(0); + } if (key.backspace || key.escape) back(); for (const filter of STATUS_FILTERS) { @@ -104,6 +112,11 @@ export function BatchListScreen() { + {totalPages > 1 + ? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · ` + : totalElements > 0 + ? `${totalElements} Einträge · ` + : ''} ↑↓ nav · Enter Details · [n] Neu · [a] Alle [P] Geplant [I] In Prod. [C] Abgeschl. [X] Storniert · Backspace Zurück diff --git a/frontend/apps/cli/src/components/production/ProductionOrderListScreen.tsx b/frontend/apps/cli/src/components/production/ProductionOrderListScreen.tsx index d66c60a..b130274 100644 --- a/frontend/apps/cli/src/components/production/ProductionOrderListScreen.tsx +++ b/frontend/apps/cli/src/components/production/ProductionOrderListScreen.tsx @@ -26,19 +26,19 @@ const STATUS_COLORS: Record = { export function ProductionOrderListScreen() { const { navigate, back } = useNavigation(); - const { productionOrders, loading, error, fetchProductionOrders, clearError } = useProductionOrders(); + const { productionOrders, loading, error, fetchProductionOrders, clearError, currentPage, totalElements, totalPages, pageSize } = useProductionOrders(); const [selectedIndex, setSelectedIndex] = useState(0); const [activeFilter, setActiveFilter] = useState<{ status?: ProductionOrderStatus; label: string }>({ label: 'Alle' }); - const loadWithFilter = useCallback((filter: { status?: ProductionOrderStatus; label: string }) => { + const loadWithFilter = useCallback((filter: { status?: ProductionOrderStatus; label: string }, pagination?: { page: number; size: number }) => { setActiveFilter(filter); setSelectedIndex(0); const f: ProductionOrderFilter | undefined = filter.status ? { status: filter.status } : undefined; - void fetchProductionOrders(f); + void fetchProductionOrders(f, pagination ?? { page: 0, size: 20 }); }, [fetchProductionOrders]); useEffect(() => { - void fetchProductionOrders(); + void fetchProductionOrders(undefined, { page: 0, size: 20 }); }, [fetchProductionOrders]); useInput((input, key) => { @@ -52,11 +52,22 @@ export function ProductionOrderListScreen() { if (order?.id) navigate('production-order-detail', { orderId: order.id }); } if (input === 'n') navigate('production-order-create'); - if (input === 'r') loadWithFilter(activeFilter); + if (input === 'r') loadWithFilter(activeFilter, { page: currentPage, size: pageSize }); const filterDef = STATUS_FILTER_KEYS[input]; if (filterDef) loadWithFilter(filterDef); + if (key.leftArrow && currentPage > 0) { + const f: ProductionOrderFilter | undefined = activeFilter.status ? { status: activeFilter.status } : undefined; + void fetchProductionOrders(f, { page: currentPage - 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.rightArrow && currentPage < totalPages - 1) { + const f: ProductionOrderFilter | undefined = activeFilter.status ? { status: activeFilter.status } : undefined; + void fetchProductionOrders(f, { page: currentPage + 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.backspace || key.escape) back(); }); @@ -108,6 +119,11 @@ export function ProductionOrderListScreen() { + {totalPages > 1 + ? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · ` + : totalElements > 0 + ? `${totalElements} Einträge · ` + : ''} ↑↓ nav · Enter Details · [n] Neu · [r] Aktualisieren · [a]lle [p]lan [f]rei [i]n Prod [c]omp [x]storno · Bksp Zurück diff --git a/frontend/apps/cli/src/components/production/RecipeListScreen.tsx b/frontend/apps/cli/src/components/production/RecipeListScreen.tsx index c0058b6..0c1359e 100644 --- a/frontend/apps/cli/src/components/production/RecipeListScreen.tsx +++ b/frontend/apps/cli/src/components/production/RecipeListScreen.tsx @@ -22,12 +22,12 @@ const STATUS_COLORS: Record = { export function RecipeListScreen() { const { navigate, back } = useNavigation(); - const { recipes, loading, error, fetchRecipes, clearError } = useRecipes(); + const { recipes, loading, error, fetchRecipes, clearError, currentPage, totalElements, totalPages, pageSize } = useRecipes(); const [selectedIndex, setSelectedIndex] = useState(0); const [statusFilter, setStatusFilter] = useState(undefined); useEffect(() => { - void fetchRecipes(statusFilter); + void fetchRecipes(statusFilter, { page: 0, size: 20 }); }, [fetchRecipes, statusFilter]); useInput((input, key) => { @@ -41,6 +41,14 @@ export function RecipeListScreen() { if (recipe) navigate('recipe-detail', { recipeId: recipe.id }); } if (input === 'n') navigate('recipe-create'); + if (key.leftArrow && currentPage > 0) { + void fetchRecipes(statusFilter, { page: currentPage - 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.rightArrow && currentPage < totalPages - 1) { + void fetchRecipes(statusFilter, { page: currentPage + 1, size: pageSize }); + setSelectedIndex(0); + } if (key.backspace || key.escape) back(); // Status-Filter @@ -100,6 +108,11 @@ export function RecipeListScreen() { + {totalPages > 1 + ? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · ` + : totalElements > 0 + ? `${totalElements} Einträge · ` + : ''} ↑↓ nav · Enter Details · [n] Neu · [a] Alle [D] Draft [A] Active [R] Archived · Backspace Zurück diff --git a/frontend/apps/cli/src/components/users/UserListScreen.tsx b/frontend/apps/cli/src/components/users/UserListScreen.tsx index 960d4f9..f806c34 100644 --- a/frontend/apps/cli/src/components/users/UserListScreen.tsx +++ b/frontend/apps/cli/src/components/users/UserListScreen.tsx @@ -8,11 +8,11 @@ import { UserTable } from './UserTable.js'; export function UserListScreen() { const { navigate, back } = useNavigation(); - const { users, loading, error, fetchUsers, clearError } = useUsers(); + const { users, loading, error, fetchUsers, clearError, currentPage, totalElements, totalPages, pageSize } = useUsers(); const [selectedIndex, setSelectedIndex] = useState(0); useEffect(() => { - void fetchUsers(); + void fetchUsers({ page: 0, size: 20 }); }, [fetchUsers]); useInput((input, key) => { @@ -33,6 +33,14 @@ export function UserListScreen() { if (input === 'n') { navigate('user-create'); } + if (key.leftArrow && currentPage > 0) { + void fetchUsers({ page: currentPage - 1, size: pageSize }); + setSelectedIndex(0); + } + if (key.rightArrow && currentPage < totalPages - 1) { + void fetchUsers({ page: currentPage + 1, size: pageSize }); + setSelectedIndex(0); + } if (key.backspace || key.escape) { back(); } @@ -61,6 +69,11 @@ export function UserListScreen() { + {totalPages > 1 + ? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · ` + : totalElements > 0 + ? `${totalElements} Einträge · ` + : ''} ↑↓ navigieren · Enter Details · [n] Neu · Backspace Zurück diff --git a/frontend/apps/cli/src/hooks/useArticles.ts b/frontend/apps/cli/src/hooks/useArticles.ts index 4162867..3e92e99 100644 --- a/frontend/apps/cli/src/hooks/useArticles.ts +++ b/frontend/apps/cli/src/hooks/useArticles.ts @@ -6,12 +6,17 @@ import type { AddSalesUnitRequest, UpdateSalesUnitPriceRequest, } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; interface ArticlesState { articles: ArticleDTO[]; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -23,13 +28,26 @@ export function useArticles() { articles: [], loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchArticles = useCallback(async () => { + const fetchArticles = useCallback(async (pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const articles = await client.articles.list(); - setState({ articles, loading: false, error: null }); + const res = await client.articles.list(pagination); + setState((s) => ({ + ...s, + articles: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } @@ -39,7 +57,7 @@ export function useArticles() { setState((s) => ({ ...s, loading: true, error: null })); try { const article = await client.articles.create(request); - setState((s) => ({ articles: [...s.articles, article], loading: false, error: null })); + setState((s) => ({ ...s, articles: [...s.articles, article], loading: false, error: null })); return article; } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); @@ -162,6 +180,22 @@ export function useArticles() { } }, []); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); @@ -178,6 +212,9 @@ export function useArticles() { updateSalesUnitPrice, assignSupplier, removeSupplier, + nextPage, + prevPage, + goToPage, clearError, }; } diff --git a/frontend/apps/cli/src/hooks/useBatches.ts b/frontend/apps/cli/src/hooks/useBatches.ts index 2536e04..308e266 100644 --- a/frontend/apps/cli/src/hooks/useBatches.ts +++ b/frontend/apps/cli/src/hooks/useBatches.ts @@ -1,5 +1,6 @@ import { useState, useCallback } from 'react'; import type { BatchSummaryDTO, BatchDTO, PlanBatchRequest, BatchStatus } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; interface BatchesState { @@ -7,6 +8,10 @@ interface BatchesState { batch: BatchDTO | null; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -19,13 +24,26 @@ export function useBatches() { batch: null, loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchBatches = useCallback(async (status?: BatchStatus) => { + const fetchBatches = useCallback(async (status?: BatchStatus, pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const batches = await client.batches.list(status); - setState((s) => ({ ...s, batches, loading: false, error: null })); + const res = await client.batches.list(status, pagination); + setState((s) => ({ + ...s, + batches: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } @@ -102,6 +120,22 @@ export function useBatches() { } }, []); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); @@ -115,6 +149,9 @@ export function useBatches() { recordConsumption, completeBatch, cancelBatch, + nextPage, + prevPage, + goToPage, clearError, }; } diff --git a/frontend/apps/cli/src/hooks/useCategories.ts b/frontend/apps/cli/src/hooks/useCategories.ts index fc6e759..75651c1 100644 --- a/frontend/apps/cli/src/hooks/useCategories.ts +++ b/frontend/apps/cli/src/hooks/useCategories.ts @@ -1,11 +1,16 @@ import { useState, useCallback } from 'react'; import type { ProductCategoryDTO } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; interface CategoriesState { categories: ProductCategoryDTO[]; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -17,13 +22,26 @@ export function useCategories() { categories: [], loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchCategories = useCallback(async () => { + const fetchCategories = useCallback(async (pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const categories = await client.categories.list(); - setState({ categories, loading: false, error: null }); + const res = await client.categories.list(pagination); + setState((s) => ({ + ...s, + categories: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } @@ -35,7 +53,7 @@ export function useCategories() { const req: Record = { name }; if (description) req.description = description; const cat = await client.categories.create(req as { name: string; description?: string }); - setState((s) => ({ categories: [...s.categories, cat], loading: false, error: null })); + setState((s) => ({ ...s, categories: [...s.categories, cat], loading: false, error: null })); return cat; } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); @@ -76,6 +94,22 @@ export function useCategories() { } }, []); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); @@ -86,6 +120,9 @@ export function useCategories() { createCategory, updateCategory, deleteCategory, + nextPage, + prevPage, + goToPage, clearError, }; } diff --git a/frontend/apps/cli/src/hooks/useCustomers.ts b/frontend/apps/cli/src/hooks/useCustomers.ts index b3c5122..f205042 100644 --- a/frontend/apps/cli/src/hooks/useCustomers.ts +++ b/frontend/apps/cli/src/hooks/useCustomers.ts @@ -6,12 +6,17 @@ import type { UpdateCustomerRequest, AddDeliveryAddressRequest, } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; interface CustomersState { customers: CustomerDTO[]; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -23,13 +28,26 @@ export function useCustomers() { customers: [], loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchCustomers = useCallback(async () => { + const fetchCustomers = useCallback(async (pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const customers = await client.customers.list(); - setState({ customers, loading: false, error: null }); + const res = await client.customers.list(pagination); + setState((s) => ({ + ...s, + customers: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } @@ -39,7 +57,7 @@ export function useCustomers() { setState((s) => ({ ...s, loading: true, error: null })); try { const customer = await client.customers.create(request); - setState((s) => ({ customers: [...s.customers, customer], loading: false, error: null })); + setState((s) => ({ ...s, customers: [...s.customers, customer], loading: false, error: null })); return customer; } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); @@ -131,6 +149,22 @@ export function useCustomers() { } }, []); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); @@ -145,6 +179,9 @@ export function useCustomers() { addDeliveryAddress, removeDeliveryAddress, setPreferences, + nextPage, + prevPage, + goToPage, clearError, }; } diff --git a/frontend/apps/cli/src/hooks/useInventoryCounts.ts b/frontend/apps/cli/src/hooks/useInventoryCounts.ts index 37bdbdf..4a685f4 100644 --- a/frontend/apps/cli/src/hooks/useInventoryCounts.ts +++ b/frontend/apps/cli/src/hooks/useInventoryCounts.ts @@ -5,6 +5,7 @@ import type { RecordCountItemRequest, InventoryCountFilter, } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; interface InventoryCountsState { @@ -12,6 +13,10 @@ interface InventoryCountsState { inventoryCount: InventoryCountDTO | null; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -24,13 +29,26 @@ export function useInventoryCounts() { inventoryCount: null, loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchInventoryCounts = useCallback(async (filter?: InventoryCountFilter) => { + const fetchInventoryCounts = useCallback(async (filter?: InventoryCountFilter, pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const inventoryCounts = await client.inventoryCounts.list(filter); - setState((s) => ({ ...s, inventoryCounts, loading: false, error: null })); + const res = await client.inventoryCounts.list(filter, pagination); + setState((s) => ({ + ...s, + inventoryCounts: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } @@ -106,6 +124,22 @@ export function useInventoryCounts() { } }, []); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); @@ -119,6 +153,9 @@ export function useInventoryCounts() { recordCountItem, completeInventoryCount, cancelInventoryCount, + nextPage, + prevPage, + goToPage, clearError, }; } diff --git a/frontend/apps/cli/src/hooks/useProductionOrders.ts b/frontend/apps/cli/src/hooks/useProductionOrders.ts index e76f70a..286dd0c 100644 --- a/frontend/apps/cli/src/hooks/useProductionOrders.ts +++ b/frontend/apps/cli/src/hooks/useProductionOrders.ts @@ -1,5 +1,6 @@ import { useState, useCallback } from 'react'; import type { ProductionOrderDTO, CreateProductionOrderRequest, ProductionOrderFilter } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; interface ProductionOrdersState { @@ -7,6 +8,10 @@ interface ProductionOrdersState { productionOrder: ProductionOrderDTO | null; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -19,13 +24,26 @@ export function useProductionOrders() { productionOrder: null, loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchProductionOrders = useCallback(async (filter?: ProductionOrderFilter) => { + const fetchProductionOrders = useCallback(async (filter?: ProductionOrderFilter, pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const productionOrders = await client.productionOrders.list(filter); - setState((s) => ({ ...s, productionOrders, loading: false, error: null })); + const res = await client.productionOrders.list(filter, pagination); + setState((s) => ({ + ...s, + productionOrders: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } @@ -89,6 +107,22 @@ export function useProductionOrders() { } }, []); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); @@ -101,6 +135,9 @@ export function useProductionOrders() { releaseProductionOrder, rescheduleProductionOrder, startProductionOrder, + nextPage, + prevPage, + goToPage, clearError, }; } diff --git a/frontend/apps/cli/src/hooks/useRecipes.ts b/frontend/apps/cli/src/hooks/useRecipes.ts index 9f7b19b..f685455 100644 --- a/frontend/apps/cli/src/hooks/useRecipes.ts +++ b/frontend/apps/cli/src/hooks/useRecipes.ts @@ -1,11 +1,16 @@ import { useState, useCallback } from 'react'; import type { RecipeSummaryDTO, CreateRecipeRequest, RecipeStatus } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; interface RecipesState { recipes: RecipeSummaryDTO[]; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -17,13 +22,26 @@ export function useRecipes() { recipes: [], loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchRecipes = useCallback(async (status?: RecipeStatus) => { + const fetchRecipes = useCallback(async (status?: RecipeStatus, pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const recipes = await client.recipes.list(status); - setState({ recipes, loading: false, error: null }); + const res = await client.recipes.list(status, pagination); + setState((s) => ({ + ...s, + recipes: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } @@ -33,8 +51,17 @@ export function useRecipes() { setState((s) => ({ ...s, loading: true, error: null })); try { const recipe = await client.recipes.create(request); - const recipes = await client.recipes.list(); - setState({ recipes, loading: false, error: null }); + const res = await client.recipes.list(); + setState((s) => ({ + ...s, + recipes: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); return recipe; } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); @@ -42,6 +69,22 @@ export function useRecipes() { } }, []); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); @@ -50,6 +93,9 @@ export function useRecipes() { ...state, fetchRecipes, createRecipe, + nextPage, + prevPage, + goToPage, clearError, }; } diff --git a/frontend/apps/cli/src/hooks/useRoles.ts b/frontend/apps/cli/src/hooks/useRoles.ts index 3b48f17..6cf58e8 100644 --- a/frontend/apps/cli/src/hooks/useRoles.ts +++ b/frontend/apps/cli/src/hooks/useRoles.ts @@ -1,11 +1,16 @@ import { useState, useCallback } from 'react'; import type { RoleDTO } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; interface RolesState { roles: RoleDTO[]; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -17,21 +22,50 @@ export function useRoles() { roles: [], loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchRoles = useCallback(async () => { + const fetchRoles = useCallback(async (pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const roles = await client.roles.list(); - setState({ roles, loading: false, error: null }); + const res = await client.roles.list(pagination); + setState((s) => ({ + ...s, + roles: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } }, []); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); - return { ...state, fetchRoles, clearError }; + return { ...state, fetchRoles, nextPage, prevPage, goToPage, clearError }; } diff --git a/frontend/apps/cli/src/hooks/useStockMovements.ts b/frontend/apps/cli/src/hooks/useStockMovements.ts index ef24b0e..9810089 100644 --- a/frontend/apps/cli/src/hooks/useStockMovements.ts +++ b/frontend/apps/cli/src/hooks/useStockMovements.ts @@ -1,5 +1,6 @@ import { useState, useCallback } from 'react'; import type { StockMovementDTO, RecordStockMovementRequest, StockMovementFilter } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; interface StockMovementsState { @@ -7,6 +8,10 @@ interface StockMovementsState { movement: StockMovementDTO | null; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -19,13 +24,26 @@ export function useStockMovements() { movement: null, loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchMovements = useCallback(async (filter?: StockMovementFilter) => { + const fetchMovements = useCallback(async (filter?: StockMovementFilter, pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const movements = await client.stockMovements.list(filter); - setState((s) => ({ ...s, movements, loading: false, error: null })); + const res = await client.stockMovements.list(filter, pagination); + setState((s) => ({ + ...s, + movements: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } @@ -53,6 +71,22 @@ export function useStockMovements() { } }, []); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); @@ -62,6 +96,9 @@ export function useStockMovements() { fetchMovements, fetchMovement, recordMovement, + nextPage, + prevPage, + goToPage, clearError, }; } diff --git a/frontend/apps/cli/src/hooks/useStocks.ts b/frontend/apps/cli/src/hooks/useStocks.ts index ac2cafa..f1ca2f0 100644 --- a/frontend/apps/cli/src/hooks/useStocks.ts +++ b/frontend/apps/cli/src/hooks/useStocks.ts @@ -8,6 +8,7 @@ import type { StockFilter, ReserveStockRequest, } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; interface StocksState { @@ -15,6 +16,10 @@ interface StocksState { stock: StockDTO | null; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -27,13 +32,26 @@ export function useStocks() { stock: null, loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchStocks = useCallback(async (filter?: StockFilter) => { + const fetchStocks = useCallback(async (filter?: StockFilter, pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const stocks = await client.stocks.list(filter); - setState((s) => ({ ...s, stocks, loading: false, error: null })); + const res = await client.stocks.list(filter, pagination); + setState((s) => ({ + ...s, + stocks: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } @@ -163,6 +181,22 @@ export function useStocks() { } }, []); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); @@ -180,6 +214,9 @@ export function useStocks() { reserveStock, releaseReservation, confirmReservation, + nextPage, + prevPage, + goToPage, clearError, }; } diff --git a/frontend/apps/cli/src/hooks/useStorageLocations.ts b/frontend/apps/cli/src/hooks/useStorageLocations.ts index 8db0892..643eebc 100644 --- a/frontend/apps/cli/src/hooks/useStorageLocations.ts +++ b/frontend/apps/cli/src/hooks/useStorageLocations.ts @@ -5,12 +5,17 @@ import type { UpdateStorageLocationRequest, StorageLocationFilter, } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; interface StorageLocationsState { storageLocations: StorageLocationDTO[]; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -22,13 +27,26 @@ export function useStorageLocations() { storageLocations: [], loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchStorageLocations = useCallback(async (filter?: StorageLocationFilter) => { + const fetchStorageLocations = useCallback(async (filter?: StorageLocationFilter, pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const storageLocations = await client.storageLocations.list(filter); - setState({ storageLocations, loading: false, error: null }); + const res = await client.storageLocations.list(filter, pagination); + setState((s) => ({ + ...s, + storageLocations: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } @@ -38,7 +56,7 @@ export function useStorageLocations() { setState((s) => ({ ...s, loading: true, error: null })); try { const location = await client.storageLocations.create(request); - setState((s) => ({ storageLocations: [...s.storageLocations, location], loading: false, error: null })); + setState((s) => ({ ...s, storageLocations: [...s.storageLocations, location], loading: false, error: null })); return location; } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); @@ -88,6 +106,22 @@ export function useStorageLocations() { } }, []); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); @@ -99,6 +133,9 @@ export function useStorageLocations() { updateStorageLocation, activateStorageLocation, deactivateStorageLocation, + nextPage, + prevPage, + goToPage, clearError, }; } diff --git a/frontend/apps/cli/src/hooks/useSuppliers.ts b/frontend/apps/cli/src/hooks/useSuppliers.ts index a53ad21..dc2401a 100644 --- a/frontend/apps/cli/src/hooks/useSuppliers.ts +++ b/frontend/apps/cli/src/hooks/useSuppliers.ts @@ -7,12 +7,17 @@ import type { AddCertificateRequest, RemoveCertificateRequest, } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; interface SuppliersState { suppliers: SupplierDTO[]; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -24,13 +29,26 @@ export function useSuppliers() { suppliers: [], loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchSuppliers = useCallback(async () => { + const fetchSuppliers = useCallback(async (pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const suppliers = await client.suppliers.list(); - setState({ suppliers, loading: false, error: null }); + const res = await client.suppliers.list(pagination); + setState((s) => ({ + ...s, + suppliers: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } @@ -40,7 +58,7 @@ export function useSuppliers() { setState((s) => ({ ...s, loading: true, error: null })); try { const supplier = await client.suppliers.create(request); - setState((s) => ({ suppliers: [...s.suppliers, supplier], loading: false, error: null })); + setState((s) => ({ ...s, suppliers: [...s.suppliers, supplier], loading: false, error: null })); return supplier; } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); @@ -132,6 +150,22 @@ export function useSuppliers() { } }, []); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); @@ -146,6 +180,9 @@ export function useSuppliers() { rateSupplier, addCertificate, removeCertificate, + nextPage, + prevPage, + goToPage, clearError, }; } diff --git a/frontend/apps/cli/src/hooks/useUsers.ts b/frontend/apps/cli/src/hooks/useUsers.ts index 69403e0..8415803 100644 --- a/frontend/apps/cli/src/hooks/useUsers.ts +++ b/frontend/apps/cli/src/hooks/useUsers.ts @@ -1,5 +1,6 @@ import { useState, useCallback } from 'react'; import type { UserDTO, CreateUserRequest } from '@effigenix/api-client'; +import type { PaginationParams } from '@effigenix/types'; import { client } from '../utils/api-client.js'; type RoleName = CreateUserRequest['roleNames'][number]; @@ -8,6 +9,10 @@ interface UsersState { users: UserDTO[]; loading: boolean; error: string | null; + currentPage: number; + totalElements: number; + totalPages: number; + pageSize: number; } function errorMessage(err: unknown): string { @@ -19,13 +24,26 @@ export function useUsers() { users: [], loading: false, error: null, + currentPage: 0, + totalElements: 0, + totalPages: 0, + pageSize: 20, }); - const fetchUsers = useCallback(async () => { + const fetchUsers = useCallback(async (pagination?: PaginationParams) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const users = await client.users.list(); - setState({ users, loading: false, error: null }); + const res = await client.users.list(pagination); + setState((s) => ({ + ...s, + users: res.content, + currentPage: res.page.number, + totalElements: res.page.totalElements, + totalPages: res.page.totalPages, + pageSize: res.page.size, + loading: false, + error: null, + })); } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); } @@ -41,7 +59,7 @@ export function useUsers() { password, roleNames: roleName ? [roleName as RoleName] : [], }); - setState((s) => ({ users: [...s.users, user], loading: false, error: null })); + setState((s) => ({ ...s, users: [...s.users, user], loading: false, error: null })); return user; } catch (err) { setState((s) => ({ ...s, loading: false, error: errorMessage(err) })); @@ -121,6 +139,22 @@ export function useUsers() { [], ); + const nextPage = useCallback(() => { + if (state.currentPage < state.totalPages - 1) { + // Caller should re-fetch with new page + } + }, [state.currentPage, state.totalPages]); + + const prevPage = useCallback(() => { + if (state.currentPage > 0) { + // Caller should re-fetch with new page + } + }, [state.currentPage]); + + const goToPage = useCallback((_page: number) => { + // Caller should re-fetch with new page + }, []); + const clearError = useCallback(() => { setState((s) => ({ ...s, error: null })); }, []); @@ -134,6 +168,9 @@ export function useUsers() { assignRole, removeRole, changePassword, + nextPage, + prevPage, + goToPage, clearError, }; } diff --git a/frontend/packages/api-client/src/resources/articles.ts b/frontend/packages/api-client/src/resources/articles.ts index 7c101bb..0b4da79 100644 --- a/frontend/packages/api-client/src/resources/articles.ts +++ b/frontend/packages/api-client/src/resources/articles.ts @@ -17,6 +17,8 @@ import type { UpdateArticleRequest, AddSalesUnitRequest, UpdateSalesUnitPriceRequest, + PaginationParams, + PagedResponse, } from '@effigenix/types'; export type Unit = 'PIECE_FIXED' | 'KG' | 'HUNDRED_GRAM' | 'PIECE_VARIABLE'; @@ -48,8 +50,12 @@ export type { export function createArticlesResource(client: AxiosInstance) { return { - async list(): Promise { - const res = await client.get('/api/articles'); + async list(pagination?: PaginationParams): Promise> { + const params: Record = {}; + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const res = await client.get>('/api/articles', { params }); return res.data; }, diff --git a/frontend/packages/api-client/src/resources/batches.ts b/frontend/packages/api-client/src/resources/batches.ts index f13ca07..827cee0 100644 --- a/frontend/packages/api-client/src/resources/batches.ts +++ b/frontend/packages/api-client/src/resources/batches.ts @@ -9,6 +9,8 @@ import type { CompleteBatchRequest, RecordConsumptionRequest, CancelBatchRequest, + PaginationParams, + PagedResponse, } from '@effigenix/types'; export type BatchStatus = 'PLANNED' | 'IN_PRODUCTION' | 'COMPLETED' | 'CANCELLED'; @@ -34,10 +36,13 @@ const BASE = '/api/production/batches'; export function createBatchesResource(client: AxiosInstance) { return { - async list(status?: BatchStatus): Promise { - const params: Record = {}; + async list(status?: BatchStatus, pagination?: PaginationParams): Promise> { + const params: Record = {}; if (status) params.status = status; - const res = await client.get(BASE, { params }); + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const res = await client.get>(BASE, { params }); return res.data; }, diff --git a/frontend/packages/api-client/src/resources/categories.ts b/frontend/packages/api-client/src/resources/categories.ts index 0ca2526..123a4cb 100644 --- a/frontend/packages/api-client/src/resources/categories.ts +++ b/frontend/packages/api-client/src/resources/categories.ts @@ -11,6 +11,8 @@ import type { ProductCategoryDTO, CreateCategoryRequest, UpdateCategoryRequest, + PaginationParams, + PagedResponse, } from '@effigenix/types'; export type { @@ -23,8 +25,12 @@ export type { export function createCategoriesResource(client: AxiosInstance) { return { - async list(): Promise { - const res = await client.get('/api/categories'); + async list(pagination?: PaginationParams): Promise> { + const params: Record = {}; + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const res = await client.get>('/api/categories', { params }); return res.data; }, diff --git a/frontend/packages/api-client/src/resources/countries.ts b/frontend/packages/api-client/src/resources/countries.ts index aff8ca8..5ad9824 100644 --- a/frontend/packages/api-client/src/resources/countries.ts +++ b/frontend/packages/api-client/src/resources/countries.ts @@ -1,14 +1,17 @@ import type { AxiosInstance } from 'axios'; -import type { CountryDTO } from '@effigenix/types'; +import type { CountryDTO, PaginationParams, PagedResponse } from '@effigenix/types'; export type { CountryDTO }; export function createCountriesResource(client: AxiosInstance) { return { - async search(query?: string): Promise { - const res = await client.get('/api/countries', { - params: query ? { q: query } : {}, - }); + async search(query?: string, pagination?: PaginationParams): Promise> { + const params: Record = {}; + if (query) params.q = query; + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const res = await client.get>('/api/countries', { params }); return res.data; }, }; diff --git a/frontend/packages/api-client/src/resources/customers.ts b/frontend/packages/api-client/src/resources/customers.ts index 0a80655..673292e 100644 --- a/frontend/packages/api-client/src/resources/customers.ts +++ b/frontend/packages/api-client/src/resources/customers.ts @@ -21,6 +21,8 @@ import type { UpdateCustomerRequest, AddDeliveryAddressRequest, SetFrameContractRequest, + PaginationParams, + PagedResponse, } from '@effigenix/types'; export type CustomerType = 'B2B' | 'B2C'; @@ -68,8 +70,12 @@ export type { export function createCustomersResource(client: AxiosInstance) { return { - async list(): Promise { - const res = await client.get('/api/customers'); + async list(pagination?: PaginationParams): Promise> { + const params: Record = {}; + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const res = await client.get>('/api/customers', { params }); return res.data; }, diff --git a/frontend/packages/api-client/src/resources/inventory-counts.ts b/frontend/packages/api-client/src/resources/inventory-counts.ts index 66a1ea1..69752ed 100644 --- a/frontend/packages/api-client/src/resources/inventory-counts.ts +++ b/frontend/packages/api-client/src/resources/inventory-counts.ts @@ -7,6 +7,8 @@ import type { RecordCountItemRequest, CancelInventoryCountRequest, InventoryCountStatus, + PaginationParams, + PagedResponse, } from '@effigenix/types'; export type { InventoryCountDTO, CreateInventoryCountRequest, RecordCountItemRequest, CancelInventoryCountRequest, InventoryCountStatus }; @@ -27,11 +29,14 @@ const BASE = '/api/inventory/inventory-counts'; export function createInventoryCountsResource(client: AxiosInstance) { return { - async list(filter?: InventoryCountFilter): Promise { - const params: Record = {}; + async list(filter?: InventoryCountFilter, pagination?: PaginationParams): Promise> { + const params: Record = {}; if (filter?.storageLocationId) params.storageLocationId = filter.storageLocationId; if (filter?.status) params.status = filter.status; - const res = await client.get(BASE, { params }); + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const res = await client.get>(BASE, { params }); return res.data; }, diff --git a/frontend/packages/api-client/src/resources/production-orders.ts b/frontend/packages/api-client/src/resources/production-orders.ts index 1a9b91c..251da68 100644 --- a/frontend/packages/api-client/src/resources/production-orders.ts +++ b/frontend/packages/api-client/src/resources/production-orders.ts @@ -1,7 +1,7 @@ /** Production Orders resource – Production BC. */ import type { AxiosInstance } from 'axios'; -import type { ProductionOrderDTO, CreateProductionOrderRequest, RescheduleProductionOrderRequest } from '@effigenix/types'; +import type { ProductionOrderDTO, CreateProductionOrderRequest, RescheduleProductionOrderRequest, PaginationParams, PagedResponse } from '@effigenix/types'; export type Priority = 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT'; @@ -34,12 +34,15 @@ const BASE = '/api/production/production-orders'; export function createProductionOrdersResource(client: AxiosInstance) { return { - async list(filter?: ProductionOrderFilter): Promise { - const params: Record = {}; + async list(filter?: ProductionOrderFilter, pagination?: PaginationParams): Promise> { + const params: Record = {}; if (filter?.status) params.status = filter.status; if (filter?.dateFrom) params.dateFrom = filter.dateFrom; if (filter?.dateTo) params.dateTo = filter.dateTo; - const res = await client.get(BASE, { params }); + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const res = await client.get>(BASE, { params }); return res.data; }, diff --git a/frontend/packages/api-client/src/resources/recipes.ts b/frontend/packages/api-client/src/resources/recipes.ts index a8c8792..caee5bb 100644 --- a/frontend/packages/api-client/src/resources/recipes.ts +++ b/frontend/packages/api-client/src/resources/recipes.ts @@ -9,6 +9,8 @@ import type { CreateRecipeRequest, AddRecipeIngredientRequest, AddProductionStepRequest, + PaginationParams, + PagedResponse, } from '@effigenix/types'; export type RecipeType = 'RAW_MATERIAL' | 'INTERMEDIATE' | 'FINISHED_PRODUCT'; @@ -47,10 +49,13 @@ const BASE = '/api/recipes'; export function createRecipesResource(client: AxiosInstance) { return { - async list(status?: RecipeStatus): Promise { - const params: Record = {}; + async list(status?: RecipeStatus, pagination?: PaginationParams): Promise> { + const params: Record = {}; if (status) params['status'] = status; - const res = await client.get(BASE, { params }); + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const res = await client.get>(BASE, { params }); return res.data; }, diff --git a/frontend/packages/api-client/src/resources/roles.ts b/frontend/packages/api-client/src/resources/roles.ts index 4ce7112..34db65c 100644 --- a/frontend/packages/api-client/src/resources/roles.ts +++ b/frontend/packages/api-client/src/resources/roles.ts @@ -3,15 +3,19 @@ */ import type { AxiosInstance } from 'axios'; -import type { RoleDTO } from '@effigenix/types'; +import type { RoleDTO, PaginationParams, PagedResponse } from '@effigenix/types'; import { API_PATHS } from '@effigenix/config'; export type { RoleDTO }; export function createRolesResource(client: AxiosInstance) { return { - async list(): Promise { - const response = await client.get(API_PATHS.roles.base); + async list(pagination?: PaginationParams): Promise> { + const params: Record = {}; + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const response = await client.get>(API_PATHS.roles.base, { params }); return response.data; }, }; diff --git a/frontend/packages/api-client/src/resources/stock-movements.ts b/frontend/packages/api-client/src/resources/stock-movements.ts index 8274490..8e9d87a 100644 --- a/frontend/packages/api-client/src/resources/stock-movements.ts +++ b/frontend/packages/api-client/src/resources/stock-movements.ts @@ -1,7 +1,7 @@ /** Stock Movements resource – Inventory BC. */ import type { AxiosInstance } from 'axios'; -import type { StockMovementDTO, RecordStockMovementRequest } from '@effigenix/types'; +import type { StockMovementDTO, RecordStockMovementRequest, PaginationParams, PagedResponse } from '@effigenix/types'; export type MovementType = | 'GOODS_RECEIPT' @@ -46,15 +46,18 @@ const BASE = '/api/inventory/stock-movements'; export function createStockMovementsResource(client: AxiosInstance) { return { - async list(filter?: StockMovementFilter): Promise { - const params: Record = {}; + async list(filter?: StockMovementFilter, pagination?: PaginationParams): Promise> { + const params: Record = {}; if (filter?.stockId) params.stockId = filter.stockId; if (filter?.articleId) params.articleId = filter.articleId; if (filter?.movementType) params.movementType = filter.movementType; if (filter?.batchReference) params.batchReference = filter.batchReference; if (filter?.from) params.from = filter.from; if (filter?.to) params.to = filter.to; - const res = await client.get(BASE, { params }); + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const res = await client.get>(BASE, { params }); return res.data; }, diff --git a/frontend/packages/api-client/src/resources/stocks.ts b/frontend/packages/api-client/src/resources/stocks.ts index 24bcfe7..e75f593 100644 --- a/frontend/packages/api-client/src/resources/stocks.ts +++ b/frontend/packages/api-client/src/resources/stocks.ts @@ -12,6 +12,8 @@ import type { BlockStockBatchRequest, ReservationDTO, ReserveStockRequest, + PaginationParams, + PagedResponse, } from '@effigenix/types'; export type BatchType = 'PURCHASED' | 'PRODUCED'; @@ -71,11 +73,14 @@ const BASE = '/api/inventory/stocks'; export function createStocksResource(client: AxiosInstance) { return { - async list(filter?: StockFilter): Promise { - const params: Record = {}; + async list(filter?: StockFilter, pagination?: PaginationParams): Promise> { + const params: Record = {}; if (filter?.storageLocationId) params.storageLocationId = filter.storageLocationId; if (filter?.articleId) params.articleId = filter.articleId; - const res = await client.get(BASE, { params }); + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const res = await client.get>(BASE, { params }); return res.data; }, diff --git a/frontend/packages/api-client/src/resources/storage-locations.ts b/frontend/packages/api-client/src/resources/storage-locations.ts index 468054e..a88b41c 100644 --- a/frontend/packages/api-client/src/resources/storage-locations.ts +++ b/frontend/packages/api-client/src/resources/storage-locations.ts @@ -11,6 +11,8 @@ import type { TemperatureRangeDTO, CreateStorageLocationRequest, UpdateStorageLocationRequest, + PaginationParams, + PagedResponse, } from '@effigenix/types'; export type StorageType = 'COLD_ROOM' | 'FREEZER' | 'DRY_STORAGE' | 'DISPLAY_COUNTER' | 'PRODUCTION_AREA'; @@ -41,11 +43,14 @@ const BASE = '/api/inventory/storage-locations'; export function createStorageLocationsResource(client: AxiosInstance) { return { - async list(filter?: StorageLocationFilter): Promise { - const params: Record = {}; + async list(filter?: StorageLocationFilter, pagination?: PaginationParams): Promise> { + const params: Record = {}; if (filter?.storageType) params['storageType'] = filter.storageType; if (filter?.active !== undefined) params['active'] = String(filter.active); - const res = await client.get(BASE, { params }); + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const res = await client.get>(BASE, { params }); return res.data; }, diff --git a/frontend/packages/api-client/src/resources/suppliers.ts b/frontend/packages/api-client/src/resources/suppliers.ts index 1b0609b..cb90e3f 100644 --- a/frontend/packages/api-client/src/resources/suppliers.ts +++ b/frontend/packages/api-client/src/resources/suppliers.ts @@ -22,6 +22,8 @@ import type { RateSupplierRequest, AddCertificateRequest, RemoveCertificateRequest, + PaginationParams, + PagedResponse, } from '@effigenix/types'; export type SupplierStatus = 'ACTIVE' | 'INACTIVE'; @@ -44,8 +46,12 @@ export type { export function createSuppliersResource(client: AxiosInstance) { return { - async list(): Promise { - const res = await client.get('/api/suppliers'); + async list(pagination?: PaginationParams): Promise> { + const params: Record = {}; + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const res = await client.get>('/api/suppliers', { params }); return res.data; }, diff --git a/frontend/packages/api-client/src/resources/users.ts b/frontend/packages/api-client/src/resources/users.ts index fd0b8dc..f7b8b14 100644 --- a/frontend/packages/api-client/src/resources/users.ts +++ b/frontend/packages/api-client/src/resources/users.ts @@ -11,6 +11,8 @@ import type { UpdateUserRequest, ChangePasswordRequest, AssignRoleRequest, + PaginationParams, + PagedResponse, } from '@effigenix/types'; export type { @@ -24,8 +26,12 @@ export type { export function createUsersResource(client: AxiosInstance) { return { - async list(): Promise { - const response = await client.get(API_PATHS.users.base); + async list(pagination?: PaginationParams): Promise> { + const params: Record = {}; + if (pagination?.page != null) params.page = String(pagination.page); + if (pagination?.size != null) params.size = String(pagination.size); + if (pagination?.sort) params.sort = pagination.sort; + const response = await client.get>(API_PATHS.users.base, { params }); return response.data; }, diff --git a/frontend/packages/types/src/common.ts b/frontend/packages/types/src/common.ts index 73f98e4..4251617 100644 --- a/frontend/packages/types/src/common.ts +++ b/frontend/packages/types/src/common.ts @@ -27,16 +27,21 @@ export interface ApiError { validationErrors?: ValidationError[]; } +export const DEFAULT_PAGE_SIZE = 20; +export const MAX_PAGE_SIZE = 100; + export interface PaginationParams { page?: number; size?: number; - sort?: string; + sort?: string[]; } export interface PagedResponse { content: T[]; - totalElements: number; - totalPages: number; - size: number; - number: number; + page: { + number: number; + size: number; + totalElements: number; + totalPages: number; + }; } diff --git a/justfile b/justfile index 075edc5..aed78d6 100644 --- a/justfile +++ b/justfile @@ -76,6 +76,22 @@ fuzz-regression: fuzz-single TEST: cd backend && mvn test -Pfuzz -Dtest={{ TEST }} +# ─── Load Testing ──────────────────────────────────────── + +# Gatling Load Test ausführen (startet Backend + DB via Testcontainers) +loadtest: + #!/usr/bin/env bash + set -euo pipefail + # Podman-Socket für Testcontainers bereitstellen (falls kein Docker) + if [ -z "${DOCKER_HOST:-}" ] && [ ! -S /var/run/docker.sock ]; then + PODMAN_SOCK="/run/user/$(id -u)/podman/podman.sock" + if [ -S "$PODMAN_SOCK" ]; then + export DOCKER_HOST="unix://$PODMAN_SOCK" + export TESTCONTAINERS_RYUK_DISABLED=true + fi + fi + cd loadtest && mvn gatling:test + # ─── Services ───────────────────────────────────────────── # Bugsink starten (Error Tracking) diff --git a/loadtest/src/test/java/de/effigenix/loadtest/infrastructure/LoadTestInfrastructure.java b/loadtest/src/test/java/de/effigenix/loadtest/infrastructure/LoadTestInfrastructure.java index bfa1578..948f589 100644 --- a/loadtest/src/test/java/de/effigenix/loadtest/infrastructure/LoadTestInfrastructure.java +++ b/loadtest/src/test/java/de/effigenix/loadtest/infrastructure/LoadTestInfrastructure.java @@ -100,8 +100,7 @@ public final class LoadTestInfrastructure { // Podman rootless Socket Path podmanSocket = Path.of("/run/user/" + getUid() + "/podman/podman.sock"); if (Files.exists(podmanSocket)) { - System.setProperty("DOCKER_HOST", "unix://" + podmanSocket); - System.setProperty("TESTCONTAINERS_RYUK_DISABLED", "true"); + configurePodman("unix://" + podmanSocket); System.out.println("=== Podman erkannt: " + podmanSocket + " ==="); return; } @@ -109,12 +108,17 @@ public final class LoadTestInfrastructure { // Podman root Socket Path podmanRootSocket = Path.of("/run/podman/podman.sock"); if (Files.exists(podmanRootSocket)) { - System.setProperty("DOCKER_HOST", "unix://" + podmanRootSocket); - System.setProperty("TESTCONTAINERS_RYUK_DISABLED", "true"); + configurePodman("unix://" + podmanRootSocket); System.out.println("=== Podman (root) erkannt: " + podmanRootSocket + " ==="); } } + private static void configurePodman(String dockerHost) { + System.setProperty("DOCKER_HOST", dockerHost); + System.setProperty("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", dockerHost.replace("unix://", "")); + System.setProperty("TESTCONTAINERS_RYUK_DISABLED", "true"); + } + private static String getUid() { try { var process = new ProcessBuilder("id", "-u").start(); diff --git a/loadtest/src/test/java/de/effigenix/loadtest/simulation/FullWorkloadSimulation.java b/loadtest/src/test/java/de/effigenix/loadtest/simulation/FullWorkloadSimulation.java index 6cc9c64..4f443ae 100644 --- a/loadtest/src/test/java/de/effigenix/loadtest/simulation/FullWorkloadSimulation.java +++ b/loadtest/src/test/java/de/effigenix/loadtest/simulation/FullWorkloadSimulation.java @@ -78,62 +78,61 @@ public class FullWorkloadSimulation extends Simulation { ) ).protocols(httpProtocol) .assertions( - // Global: Login (BCrypt ~230ms) hebt den Schnitt - global().responseTime().percentile(95.0).lt(500), - global().responseTime().percentile(99.0).lt(1000), - global().failedRequests().percent().lt(5.0), + // ── Global ────────────────────────────────────────── + global().responseTime().percentile(95.0).lt(250), + global().responseTime().percentile(99.0).lt(400), + global().failedRequests().percent().lt(1.0), - // Login darf langsam sein (BCrypt strength 12) - details("Login [admin]").responseTime().mean().lt(350), - details("Login [admin]").responseTime().percentile(95.0).lt(500), + // ── Login (BCrypt strength 12 ~230ms) ─────────────── + details("Login [admin]").responseTime().mean().lt(280), + details("Login [admin]").responseTime().percentile(95.0).lt(350), - // Einzeldatensatz-Reads: streng (mean < 20ms) - details("Rezept laden").responseTime().mean().lt(20), - details("Artikel laden").responseTime().mean().lt(20), - details("Lieferant laden").responseTime().mean().lt(20), - details("Kunde laden").responseTime().mean().lt(20), + // ── Einzeldatensatz-Reads: mean < 10ms ───────────── + details("Rezept laden").responseTime().mean().lt(10), + details("Artikel laden").responseTime().mean().lt(10), + details("Lieferant laden").responseTime().mean().lt(10), + details("Kunde laden").responseTime().mean().lt(10), + details("Inventur laden").responseTime().mean().lt(10), + details("Lagerort laden").responseTime().mean().lt(10), - // Listen mit wenig Daten (< 30 Einträge): mean < 35ms - details("Rezepte auflisten").responseTime().mean().lt(35), - details("Lagerorte auflisten").responseTime().mean().lt(35), - details("Bestände auflisten").responseTime().mean().lt(35), - details("Bestände unter Minimum").responseTime().mean().lt(35), - details("Bestände nach Lagerort").responseTime().mean().lt(35), - details("Lieferanten auflisten").responseTime().mean().lt(35), - details("Kategorien auflisten").responseTime().mean().lt(35), - details("Bestandsbewegungen auflisten").responseTime().mean().lt(35), - details("Bestandsbewegungen nach Bestand").responseTime().mean().lt(35), - details("Bestandsbewegungen nach Charge").responseTime().mean().lt(35), - details("Bestandsbewegungen nach Zeitraum").responseTime().mean().lt(35), - details("Inventuren auflisten").responseTime().mean().lt(35), - details("Inventur laden").responseTime().mean().lt(20), + // ── Paginated Listen (20er Seiten): mean < 15ms ──── + details("Rezepte auflisten").responseTime().mean().lt(15), + details("Lagerorte auflisten").responseTime().mean().lt(15), + details("Bestände auflisten").responseTime().mean().lt(15), + details("Bestände unter Minimum").responseTime().mean().lt(15), + details("Bestände nach Lagerort").responseTime().mean().lt(15), + details("Lieferanten auflisten").responseTime().mean().lt(15), + details("Kategorien auflisten").responseTime().mean().lt(15), + details("Bestandsbewegungen auflisten").responseTime().mean().lt(15), + details("Bestandsbewegungen nach Bestand").responseTime().mean().lt(15), + details("Bestandsbewegungen nach Charge").responseTime().mean().lt(15), + details("Bestandsbewegungen nach Zeitraum").responseTime().mean().lt(15), + details("Inventuren auflisten").responseTime().mean().lt(15), + details("Produktionsaufträge auflisten").responseTime().mean().lt(15), + details("Produktionsaufträge nach Status").responseTime().mean().lt(15), - // Listen mit viel Daten (50-300 Einträge): mean < 75ms - details("Chargen auflisten").responseTime().mean().lt(75), - details("Artikel auflisten").responseTime().mean().lt(75), - details("Kunden auflisten").responseTime().mean().lt(75), + // ── Listen mit Children-Loading: mean < 25ms ─────── + details("Chargen auflisten").responseTime().mean().lt(25), + details("Artikel auflisten").responseTime().mean().lt(25), + details("Kunden auflisten").responseTime().mean().lt(25), - // Garantiert vorkommende Write-Requests: moderat (mean < 50ms) - details("Charge planen").responseTime().mean().lt(50), - details("Charge starten").responseTime().mean().lt(50), - details("Charge abschließen").responseTime().mean().lt(50), - details("Produktionsauftrag anlegen").responseTime().mean().lt(50), - details("Produktionsauftrag freigeben").responseTime().mean().lt(50), - details("Produktionsauftrag abschließen").responseTime().mean().lt(50), - details("Produktionsauftrag stornieren").responseTime().mean().lt(50), - details("Produktionsauftrag umterminieren").responseTime().mean().lt(50), - details("Bestandsbewegung erfassen").responseTime().mean().lt(50), - details("Inventur anlegen").responseTime().mean().lt(50), - details("Inventur starten").responseTime().mean().lt(50), - details("Ist-Menge erfassen").responseTime().mean().lt(50), + // ── Write-Requests: mean < 20ms ──────────────────── + details("Charge planen").responseTime().mean().lt(20), + details("Charge starten").responseTime().mean().lt(20), + details("Charge abschließen").responseTime().mean().lt(20), + details("Produktionsauftrag anlegen").responseTime().mean().lt(20), + details("Produktionsauftrag freigeben").responseTime().mean().lt(20), + details("Produktionsauftrag abschließen").responseTime().mean().lt(20), + details("Produktionsauftrag stornieren").responseTime().mean().lt(20), + details("Produktionsauftrag umterminieren").responseTime().mean().lt(20), + details("Bestandsbewegung erfassen").responseTime().mean().lt(20), + details("Inventur anlegen").responseTime().mean().lt(20), + details("Inventur starten").responseTime().mean().lt(20), + details("Ist-Menge erfassen").responseTime().mean().lt(20), - // Tracing: BFS-Traversierung, mean < 50ms - details("Charge vorwärts tracen").responseTime().mean().lt(50), - details("Charge rückwärts tracen").responseTime().mean().lt(50), - - // Produktionsaufträge-Listen: mean < 35ms - details("Produktionsaufträge auflisten").responseTime().mean().lt(35), - details("Produktionsaufträge nach Status").responseTime().mean().lt(35) + // ── Tracing (BFS): mean < 15ms ───────────────────── + details("Charge vorwärts tracen").responseTime().mean().lt(15), + details("Charge rückwärts tracen").responseTime().mean().lt(15) ); } }