import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { catalogApi, type Product, type Warehouse } from '../api/catalogApi'; import { getApiErrorMessage } from '../api/errors'; import type { PageResponse } from '../api/types'; import { type ManualStockAdjustmentPayload, type StockBalance, warehouseApi } from '../api/warehouseApi'; import { useAuth } from '../auth/AuthContext'; import { DataTable, type TableColumn } from '../components/DataTable'; import { Layout } from '../components/Layout'; import { StockAdjustmentForm } from '../components/StockAdjustmentForm'; import { Button } from '../components/ui/Button'; import { ErrorState } from '../components/ui/ErrorState'; import { Input } from '../components/ui/Input'; import { LoadingState } from '../components/ui/LoadingState'; import { PageHeader } from '../components/ui/PageHeader'; import { Pagination } from '../components/ui/Pagination'; import { Select } from '../components/ui/Select'; import { formatDateTime, formatQuantity } from '../utils/formatters'; const PAGE_SIZE = 20; export function StockBalancesPage() { const { user } = useAuth(); const canAdjust = user?.role === 'ADMIN' || user?.role === 'WAREHOUSE'; const [pageData, setPageData] = useState | null>(null); const [warehouses, setWarehouses] = useState([]); const [products, setProducts] = useState([]); const [page, setPage] = useState(0); const [searchInput, setSearchInput] = useState(''); const [search, setSearch] = useState(''); const [warehouseId, setWarehouseId] = useState(''); const [productId, setProductId] = useState(''); const [showAdjustmentForm, setShowAdjustmentForm] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); useEffect(() => { let isMounted = true; async function loadFilters() { try { const [warehousePage, productPage] = await Promise.all([ catalogApi.listWarehouses({ page: 0, size: 100, active: true }), catalogApi.listProducts({ page: 0, size: 100, active: true }), ]); if (!isMounted) { return; } setWarehouses(warehousePage.items); setProducts(productPage.items); } catch (caughtError) { if (isMounted) { setError(getApiErrorMessage(caughtError, 'Не удалось загрузить фильтры')); } } } void loadFilters(); return () => { isMounted = false; }; }, []); const loadData = useCallback(async () => { setIsLoading(true); setError(''); try { const data = await warehouseApi.listStockBalances({ page, size: PAGE_SIZE, search, warehouseId, productId, }); setPageData(data); } catch (caughtError) { setError(getApiErrorMessage(caughtError, 'Не удалось загрузить остатки')); } finally { setIsLoading(false); } }, [page, productId, search, warehouseId]); useEffect(() => { void loadData(); }, [loadData]); const columns = useMemo[]>( () => [ { key: 'warehouse', label: 'Склад', render: (item) => `${item.warehouse.code} / ${item.warehouse.name}` }, { key: 'sku', label: 'SKU товара', render: (item) => item.product.sku }, { key: 'product', label: 'Товар', render: (item) => item.product.name }, { key: 'unit', label: 'Ед. изм.', render: (item) => item.product.unit }, { key: 'quantityOnHand', label: 'Остаток', render: (item) => formatQuantity(item.quantityOnHand) }, { key: 'updatedAt', label: 'Обновлено', render: (item) => formatDateTime(item.updatedAt) }, ], [], ); const handleSearchSubmit = (event: FormEvent) => { event.preventDefault(); setPage(0); setSearch(searchInput.trim()); }; const handleAdjustment = async (payload: ManualStockAdjustmentPayload) => { await warehouseApi.manualStockAdjustment(payload); setShowAdjustmentForm(false); await loadData(); }; return (
setShowAdjustmentForm((current) => !current)}> Ручная корректировка ) : null } />
setSearchInput(event.target.value)} placeholder="Поиск товара или склада" className="min-w-0" />
{showAdjustmentForm && canAdjust && ( setShowAdjustmentForm(false)} /> )} {error && } {isLoading ? ( ) : ( item.id} emptyTitle="Остатки не найдены" /> )} {pageData && ( setPage((current) => Math.max(current - 1, 0))} onNext={() => setPage((current) => current + 1)} /> )}
); }