1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 13:49:36 +01:00

feat: Paginierung für alle GET-List-Endpoints (#61)

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<T>, 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<T>
- Alle Controller mit page/size/sort Query-Params + PageResponse

Frontend:
- PagedResponse<T> 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
This commit is contained in:
Sebastian Frick 2026-03-20 16:33:20 +01:00
parent fc4faafd57
commit 72979c9537
151 changed files with 2880 additions and 1120 deletions

View file

@ -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<ArticleDTO[]> {
const res = await client.get<ArticleDTO[]>('/api/articles');
async list(pagination?: PaginationParams): Promise<PagedResponse<ArticleDTO>> {
const params: Record<string, string | string[]> = {};
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<PagedResponse<ArticleDTO>>('/api/articles', { params });
return res.data;
},

View file

@ -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<BatchSummaryDTO[]> {
const params: Record<string, string> = {};
async list(status?: BatchStatus, pagination?: PaginationParams): Promise<PagedResponse<BatchSummaryDTO>> {
const params: Record<string, string | string[]> = {};
if (status) params.status = status;
const res = await client.get<BatchSummaryDTO[]>(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<PagedResponse<BatchSummaryDTO>>(BASE, { params });
return res.data;
},

View file

@ -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<ProductCategoryDTO[]> {
const res = await client.get<ProductCategoryDTO[]>('/api/categories');
async list(pagination?: PaginationParams): Promise<PagedResponse<ProductCategoryDTO>> {
const params: Record<string, string | string[]> = {};
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<PagedResponse<ProductCategoryDTO>>('/api/categories', { params });
return res.data;
},

View file

@ -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<CountryDTO[]> {
const res = await client.get<CountryDTO[]>('/api/countries', {
params: query ? { q: query } : {},
});
async search(query?: string, pagination?: PaginationParams): Promise<PagedResponse<CountryDTO>> {
const params: Record<string, string | string[]> = {};
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<PagedResponse<CountryDTO>>('/api/countries', { params });
return res.data;
},
};

View file

@ -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<CustomerDTO[]> {
const res = await client.get<CustomerDTO[]>('/api/customers');
async list(pagination?: PaginationParams): Promise<PagedResponse<CustomerDTO>> {
const params: Record<string, string | string[]> = {};
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<PagedResponse<CustomerDTO>>('/api/customers', { params });
return res.data;
},

View file

@ -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<InventoryCountDTO[]> {
const params: Record<string, string> = {};
async list(filter?: InventoryCountFilter, pagination?: PaginationParams): Promise<PagedResponse<InventoryCountDTO>> {
const params: Record<string, string | string[]> = {};
if (filter?.storageLocationId) params.storageLocationId = filter.storageLocationId;
if (filter?.status) params.status = filter.status;
const res = await client.get<InventoryCountDTO[]>(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<PagedResponse<InventoryCountDTO>>(BASE, { params });
return res.data;
},

View file

@ -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<ProductionOrderDTO[]> {
const params: Record<string, string> = {};
async list(filter?: ProductionOrderFilter, pagination?: PaginationParams): Promise<PagedResponse<ProductionOrderDTO>> {
const params: Record<string, string | string[]> = {};
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<ProductionOrderDTO[]>(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<PagedResponse<ProductionOrderDTO>>(BASE, { params });
return res.data;
},

View file

@ -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<RecipeSummaryDTO[]> {
const params: Record<string, string> = {};
async list(status?: RecipeStatus, pagination?: PaginationParams): Promise<PagedResponse<RecipeSummaryDTO>> {
const params: Record<string, string | string[]> = {};
if (status) params['status'] = status;
const res = await client.get<RecipeSummaryDTO[]>(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<PagedResponse<RecipeSummaryDTO>>(BASE, { params });
return res.data;
},

View file

@ -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<RoleDTO[]> {
const response = await client.get<RoleDTO[]>(API_PATHS.roles.base);
async list(pagination?: PaginationParams): Promise<PagedResponse<RoleDTO>> {
const params: Record<string, string | string[]> = {};
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<PagedResponse<RoleDTO>>(API_PATHS.roles.base, { params });
return response.data;
},
};

View file

@ -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<StockMovementDTO[]> {
const params: Record<string, string> = {};
async list(filter?: StockMovementFilter, pagination?: PaginationParams): Promise<PagedResponse<StockMovementDTO>> {
const params: Record<string, string | string[]> = {};
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<StockMovementDTO[]>(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<PagedResponse<StockMovementDTO>>(BASE, { params });
return res.data;
},

View file

@ -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<StockDTO[]> {
const params: Record<string, string> = {};
async list(filter?: StockFilter, pagination?: PaginationParams): Promise<PagedResponse<StockDTO>> {
const params: Record<string, string | string[]> = {};
if (filter?.storageLocationId) params.storageLocationId = filter.storageLocationId;
if (filter?.articleId) params.articleId = filter.articleId;
const res = await client.get<StockDTO[]>(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<PagedResponse<StockDTO>>(BASE, { params });
return res.data;
},

View file

@ -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<StorageLocationDTO[]> {
const params: Record<string, string> = {};
async list(filter?: StorageLocationFilter, pagination?: PaginationParams): Promise<PagedResponse<StorageLocationDTO>> {
const params: Record<string, string | string[]> = {};
if (filter?.storageType) params['storageType'] = filter.storageType;
if (filter?.active !== undefined) params['active'] = String(filter.active);
const res = await client.get<StorageLocationDTO[]>(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<PagedResponse<StorageLocationDTO>>(BASE, { params });
return res.data;
},

View file

@ -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<SupplierDTO[]> {
const res = await client.get<SupplierDTO[]>('/api/suppliers');
async list(pagination?: PaginationParams): Promise<PagedResponse<SupplierDTO>> {
const params: Record<string, string | string[]> = {};
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<PagedResponse<SupplierDTO>>('/api/suppliers', { params });
return res.data;
},

View file

@ -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<UserDTO[]> {
const response = await client.get<UserDTO[]>(API_PATHS.users.base);
async list(pagination?: PaginationParams): Promise<PagedResponse<UserDTO>> {
const params: Record<string, string | string[]> = {};
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<PagedResponse<UserDTO>>(API_PATHS.users.base, { params });
return response.data;
},