fix: 404 при обновлении, стрелки периода, фильтры в URL, авто-категории и очистка истории
- Nginx: проксирование /api на backend (единая точка входа) - История: стрелки ← → для переключения недель/месяцев/годов - История: сохранение фильтров и пагинации в URL при F5 - Импорт: миграция 003 — дефолтные правила категорий (PYATEROCHK, AUCHAN и др.) - Настройки: вкладка «Данные» с кнопкой «Очистить историю» - Backend: DELETE /api/transactions для удаления всех транзакций - ClearHistoryModal: подтверждение чекбоксами и вводом «УДАЛИТЬ»
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import type {
|
||||
Transaction,
|
||||
Account,
|
||||
@@ -19,10 +20,26 @@ 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: '',
|
||||
@@ -37,10 +54,70 @@ function getDefaultFilters(): FiltersState {
|
||||
};
|
||||
}
|
||||
|
||||
function filtersFromParams(params: URLSearchParams): Partial<FiltersState> {
|
||||
const out: Partial<FiltersState> = {};
|
||||
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<string, unknown>)[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 [filters, setFilters] = useState<FiltersState>(getDefaultFilters);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(50);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const urlFilters = filtersFromParams(searchParams);
|
||||
const { page: urlPage, pageSize: urlPageSize } = paramsFromUrl(searchParams);
|
||||
const defaultFilters = getDefaultFilters();
|
||||
const [filters, setFilters] = useState<FiltersState>(() => ({
|
||||
...defaultFilters,
|
||||
...urlFilters,
|
||||
periodMode:
|
||||
(urlFilters.periodMode as FiltersState['periodMode']) ??
|
||||
defaultFilters.periodMode,
|
||||
}));
|
||||
const [page, setPage] = useState(urlPage);
|
||||
const [pageSize, setPageSize] = useState(urlPageSize);
|
||||
const [data, setData] = useState<PaginatedResponse<Transaction> | null>(
|
||||
null,
|
||||
);
|
||||
@@ -55,6 +132,22 @@ export function HistoryPage() {
|
||||
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 {
|
||||
@@ -92,8 +185,12 @@ export function HistoryPage() {
|
||||
const handleFiltersChange = (newFilters: FiltersState) => {
|
||||
setFilters(newFilters);
|
||||
setPage(1);
|
||||
setSearchParams(filtersToParams(newFilters, 1, pageSize), {
|
||||
replace: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleEditSave = () => {
|
||||
setEditingTx(null);
|
||||
fetchData();
|
||||
@@ -136,10 +233,20 @@ export function HistoryPage() {
|
||||
pageSize={data.pageSize}
|
||||
totalItems={data.totalItems}
|
||||
totalPages={data.totalPages}
|
||||
onPageChange={setPage}
|
||||
onPageChange={(p) => {
|
||||
setPage(p);
|
||||
setSearchParams(
|
||||
filtersToParams(filters, p, pageSize),
|
||||
{ replace: true },
|
||||
);
|
||||
}}
|
||||
onPageSizeChange={(size) => {
|
||||
setPageSize(size);
|
||||
setPage(1);
|
||||
setSearchParams(
|
||||
filtersToParams(filters, 1, size),
|
||||
{ replace: true },
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user