forked from admin/family_budget
165 lines
4.5 KiB
TypeScript
165 lines
4.5 KiB
TypeScript
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>
|
||
);
|
||
}
|