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:
parent
fc4faafd57
commit
72979c9537
151 changed files with 2880 additions and 1120 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue