Files
family_budget/frontend/src/pages/HistoryPage.tsx
2026-03-02 00:33:09 +03:00

165 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from 'react';
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';
function getDefaultFilters(): FiltersState {
const now = new Date();
const from = new Date(now.getFullYear(), now.getMonth(), 1);
return {
from: toISODate(from),
to: toISODate(now),
accountId: '',
direction: '',
categoryId: '',
search: '',
amountMin: '',
amountMax: '',
onlyUnconfirmed: false,
sortBy: 'date',
sortOrder: 'desc',
};
}
export function HistoryPage() {
const [filters, setFilters] = useState<FiltersState>(getDefaultFilters);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(50);
const [data, setData] = useState<PaginatedResponse<Transaction> | null>(
null,
);
const [loading, setLoading] = useState(false);
const [accounts, setAccounts] = useState<Account[]>([]);
const [categories, setCategories] = useState<Category[]>([]);
const [editingTx, setEditingTx] = useState<Transaction | null>(null);
const [showImport, setShowImport] = useState(false);
useEffect(() => {
getAccounts().then(setAccounts).catch(() => {});
getCategories().then(setCategories).catch(() => {});
}, []);
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);
};
const handleEditSave = () => {
setEditingTx(null);
fetchData();
};
const handleImportDone = () => {
setShowImport(false);
fetchData();
getAccounts().then(setAccounts).catch(() => {});
};
return (
<div className="page">
<div className="page-header">
<h1>История операций</h1>
<button
className="btn btn-primary"
onClick={() => setShowImport(true)}
>
Импорт выписки
</button>
</div>
<TransactionFilters
filters={filters}
onChange={handleFiltersChange}
accounts={accounts}
categories={categories}
/>
<TransactionTable
transactions={data?.items ?? []}
loading={loading}
onEdit={setEditingTx}
/>
{data && (
<Pagination
page={data.page}
pageSize={data.pageSize}
totalItems={data.totalItems}
totalPages={data.totalPages}
onPageChange={setPage}
onPageSizeChange={(size) => {
setPageSize(size);
setPage(1);
}}
/>
)}
{editingTx && (
<EditTransactionModal
transaction={editingTx}
categories={categories}
onClose={() => setEditingTx(null)}
onSave={handleEditSave}
/>
)}
{showImport && (
<ImportModal
onClose={() => setShowImport(false)}
onDone={handleImportDone}
/>
)}
</div>
);
}