feat: creats frontend for the project
This commit is contained in:
164
frontend/src/pages/HistoryPage.tsx
Normal file
164
frontend/src/pages/HistoryPage.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user