1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 08:29: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

@ -20,13 +20,13 @@ const STATUS_COLORS: Record<InventoryCountStatus, string> = {
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<StatusFilter>('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() {
<Box marginTop={1}>
<Text color="gray" dimColor>
{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
</Text>
</Box>

View file

@ -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() {
<Box marginTop={1}>
<Text color="gray" dimColor>
{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
</Text>
</Box>

View file

@ -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<string | undefined>(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() {
<Box marginTop={1}>
<Text color="gray" dimColor>
{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
</Text>
</Box>

View file

@ -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<Filter>('ALL');
const [typeFilter, setTypeFilter] = useState<StorageType | null>(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() {
<Box marginTop={1}>
<Text color="gray" dimColor>
{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
</Text>
</Box>

View file

@ -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<Filter>('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() {
<Box marginTop={1}>
<Text color="gray" dimColor>
{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
</Text>
</Box>

View file

@ -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<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(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() {
<Box marginTop={1}>
<Text color="gray" dimColor>
{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
</Text>
</Box>

View file

@ -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<StatusFilter>('ALL');
const [typeFilter, setTypeFilter] = useState<TypeFilter>('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() {
<Box marginTop={1}>
<Text color="gray" dimColor>
{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
</Text>
</Box>

View file

@ -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<Filter>('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() {
<Box marginTop={1}>
<Text color="gray" dimColor>
{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
</Text>
</Box>

View file

@ -24,12 +24,12 @@ const STATUS_COLORS: Record<string, string> = {
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<BatchStatus | undefined>(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() {
<Box marginTop={1}>
<Text color="gray" dimColor>
{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
</Text>
</Box>

View file

@ -26,19 +26,19 @@ const STATUS_COLORS: Record<string, string> = {
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() {
<Box marginTop={1}>
<Text color="gray" dimColor>
{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
</Text>
</Box>

View file

@ -22,12 +22,12 @@ const STATUS_COLORS: Record<string, string> = {
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<RecipeStatus | undefined>(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() {
<Box marginTop={1}>
<Text color="gray" dimColor>
{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
</Text>
</Box>

View file

@ -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() {
<Box marginTop={1}>
<Text color="gray" dimColor>
{totalPages > 1
? `Seite ${currentPage + 1}/${totalPages} (${totalElements} Einträge) · [←→] Seite · `
: totalElements > 0
? `${totalElements} Einträge · `
: ''}
navigieren · Enter Details · [n] Neu · Backspace Zurück
</Text>
</Box>

View file

@ -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,
};
}

View file

@ -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,
};
}

View file

@ -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<string, string> = { 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,
};
}

View file

@ -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,
};
}

View file

@ -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,
};
}

View file

@ -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,
};
}

View file

@ -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,
};
}

View file

@ -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 };
}

View file

@ -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,
};
}

View file

@ -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,
};
}

View file

@ -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,
};
}

View file

@ -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,
};
}

View file

@ -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,
};
}

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;
},

View file

@ -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<T> {
content: T[];
totalElements: number;
totalPages: number;
size: number;
number: number;
page: {
number: number;
size: number;
totalElements: number;
totalPages: number;
};
}