import { useState, useEffect, useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; import type { Transaction, Account, Category, PaginatedResponse, GetTransactionsParams, } from '@family-budget/shared'; import { getTransactions } from '../api/transactions'; import { getAccounts } from '../api/accounts'; import { getCategories } from '../api/categories'; import { TransactionFilters, type FiltersState, } from '../components/TransactionFilters'; import { TransactionTable } from '../components/TransactionTable'; import { Pagination } from '../components/Pagination'; import { EditTransactionModal } from '../components/EditTransactionModal'; import { ImportModal } from '../components/ImportModal'; import { toISODate } from '../utils/format'; const PARAM_KEYS = [ 'from', 'to', 'accountId', 'direction', 'categoryId', 'search', 'amountMin', 'amountMax', 'onlyUnconfirmed', 'sortBy', 'sortOrder', 'periodMode', ] as const; function getDefaultFilters(): FiltersState { const now = new Date(); const from = new Date(now.getFullYear(), now.getMonth(), 1); return { periodMode: 'month', from: toISODate(from), to: toISODate(now), accountId: '', direction: '', categoryId: '', search: '', amountMin: '', amountMax: '', onlyUnconfirmed: false, sortBy: 'date', sortOrder: 'desc', }; } function filtersFromParams(params: URLSearchParams): Partial { const out: Partial = {}; for (const key of PARAM_KEYS) { const v = params.get(key); if (v !== null) { if (key === 'onlyUnconfirmed') { out[key] = v === '1' || v === 'true'; } else { (out as Record)[key] = v; } } } return out; } function paramsFromUrl(params: URLSearchParams): { page: number; pageSize: number; } { const p = params.get('page'); const ps = params.get('pageSize'); return { page: p ? Math.max(1, parseInt(p, 10) || 1) : 1, pageSize: ps ? parseInt(ps, 10) || 50 : 50, }; } function filtersToParams( f: FiltersState, pageNum: number, pageSizeNum: number, ): URLSearchParams { const p = new URLSearchParams(); if (f.from) p.set('from', f.from); if (f.to) p.set('to', f.to); if (f.accountId) p.set('accountId', f.accountId); if (f.direction) p.set('direction', f.direction); if (f.categoryId) p.set('categoryId', f.categoryId); if (f.search) p.set('search', f.search); if (f.amountMin) p.set('amountMin', f.amountMin); if (f.amountMax) p.set('amountMax', f.amountMax); if (f.onlyUnconfirmed) p.set('onlyUnconfirmed', '1'); if (f.sortBy) p.set('sortBy', f.sortBy); if (f.sortOrder) p.set('sortOrder', f.sortOrder); if (f.periodMode) p.set('periodMode', f.periodMode); if (pageNum > 1) p.set('page', String(pageNum)); if (pageSizeNum !== 50) p.set('pageSize', String(pageSizeNum)); return p; } export function HistoryPage() { const [searchParams, setSearchParams] = useSearchParams(); const urlFilters = filtersFromParams(searchParams); const { page: urlPage, pageSize: urlPageSize } = paramsFromUrl(searchParams); const defaultFilters = getDefaultFilters(); const [filters, setFilters] = useState(() => ({ ...defaultFilters, ...urlFilters, periodMode: (urlFilters.periodMode as FiltersState['periodMode']) ?? defaultFilters.periodMode, })); const [page, setPage] = useState(urlPage); const [pageSize, setPageSize] = useState(urlPageSize); const [data, setData] = useState | null>( null, ); const [loading, setLoading] = useState(false); const [accounts, setAccounts] = useState([]); const [categories, setCategories] = useState([]); const [editingTx, setEditingTx] = useState(null); const [showImport, setShowImport] = useState(false); useEffect(() => { getAccounts().then(setAccounts).catch(() => {}); getCategories().then(setCategories).catch(() => {}); }, []); useEffect(() => { const urlFilters = filtersFromParams(searchParams); const { page: p, pageSize: ps } = paramsFromUrl(searchParams); if (Object.keys(urlFilters).length > 0) { setFilters((prev) => ({ ...getDefaultFilters(), ...urlFilters, periodMode: (urlFilters.periodMode as FiltersState['periodMode']) ?? prev.periodMode, })); } setPage(p); setPageSize(ps); }, [searchParams]); const fetchData = useCallback(async () => { setLoading(true); try { const params: GetTransactionsParams = { page, pageSize, sortBy: filters.sortBy, sortOrder: filters.sortOrder, }; if (filters.from) params.from = filters.from; if (filters.to) params.to = filters.to; if (filters.accountId) params.accountId = Number(filters.accountId); if (filters.direction) params.direction = filters.direction; if (filters.categoryId) params.categoryId = Number(filters.categoryId); if (filters.search) params.search = filters.search; if (filters.amountMin) params.amountMin = Math.round(Number(filters.amountMin) * 100); if (filters.amountMax) params.amountMax = Math.round(Number(filters.amountMax) * 100); if (filters.onlyUnconfirmed) params.onlyUnconfirmed = true; const result = await getTransactions(params); setData(result); } catch { // 401 handled by interceptor } finally { setLoading(false); } }, [filters, page, pageSize]); useEffect(() => { fetchData(); }, [fetchData]); const handleFiltersChange = (newFilters: FiltersState) => { setFilters(newFilters); setPage(1); setSearchParams(filtersToParams(newFilters, 1, pageSize), { replace: true, }); }; const handleEditSave = () => { setEditingTx(null); fetchData(); }; const handleImportDone = () => { setShowImport(false); fetchData(); getAccounts().then(setAccounts).catch(() => {}); }; return (

История операций

{data && ( { setPage(p); setSearchParams( filtersToParams(filters, p, pageSize), { replace: true }, ); }} onPageSizeChange={(size) => { setPageSize(size); setPage(1); setSearchParams( filtersToParams(filters, 1, size), { replace: true }, ); }} /> )} {editingTx && ( setEditingTx(null)} onSave={handleEditSave} /> )} {showImport && ( setShowImport(false)} onDone={handleImportDone} /> )}
); }