feat: creats frontend for the project

This commit is contained in:
vakabunga
2026-03-02 00:33:09 +03:00
parent 4d67636633
commit cd56e2bf9d
37 changed files with 3762 additions and 0 deletions

View 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>
);
}