Files
actual-importer/web/src/pages/PrepareTransactionsPage/PrepareTransactionsPage.tsx

167 lines
6.6 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from "react";
import { useStore } from "../../store/AppStore";
import TransactionsTable from "./TransactionsTable";
import { SkippedLines } from "./SkippedLines";
import ImportBar from "./ImportBar";
import { type ImportOptions, type Transaction } from "../../types/api";
import { getDateRangedTransactions, getLatestTransactions, submitTransactions } from "../../services/api.service";
import { useLoader } from "../../hooks/useLoader";
import { FilterPanel, type FilterData } from "./FilterPanel";
import dayjs from "dayjs";
import { ExistingTransactions } from "./ExistingTransactions";
import classNames from "classnames";
import { useDebounce } from "../../hooks/useDebounce";
export default function PrepareTransactionsPage() {
const { state, dispatch } = useStore();
const [userFilter, setUserFilter] = useState<FilterData>({});
const debouncedUserFilter = useDebounce(userFilter, 700);
const [deselected, setDeselected] = useState<number[]>([]);
const [enabledExistingTransactions, setEnabledExistingTransactions] = useState(true);
const [existingTransactions, setExistingTransactions] = useState<Transaction[]>([]);
const select = useCallback((index: number) => setDeselected(deselected.filter(x => x !== index)), [deselected, setDeselected]);
const deselect = useCallback((index: number) => setDeselected([...deselected, index]), [deselected, setDeselected]);
const filteredTransactions = useMemo(() => state.transactions
.filter(t => {
if (!userFilter.from) {
return true;
}
const date = dayjs(t.date);
return date.isAfter(userFilter.from, 'day') || date.isSame(userFilter.from, 'day');
})
.filter(t => {
if (!userFilter.to) {
return true;
}
const date = dayjs(t.date);
return date.isBefore(userFilter.to, 'day') || date.isSame(userFilter.to, 'day');
})
.slice().reverse()
, [state.transactions, userFilter]);
const selectedTransactions = useMemo(() =>
filteredTransactions.filter((_, i) => !deselected.includes(i)),
[filteredTransactions, deselected]);
const { fn: handleSubmit, loading } = useLoader(async (opts: ImportOptions) => {
if(!state.server) {
return;
}
const response = await submitTransactions(selectedTransactions.slice().reverse(), state.server, opts);
dispatch({
type: 'SET_RESULT',
payload: response
});
dispatch({
type: 'MOVE_STEP',
payload: {
offset: 1
}
});
}, [selectedTransactions, state]);
const { fn: refreshExistingTransactions, loading: loadingExistingTransactions } = useLoader(async () => {
if (!state.server || !enabledExistingTransactions) {
setExistingTransactions([]);
return;
}
// If no "from" filter active, pull the latest 10 transactions
if (!debouncedUserFilter.from) {
const transactions = await getLatestTransactions(state.server, 10, state.profile);
setExistingTransactions(transactions);
return;
}
const start = debouncedUserFilter.from.format("YYYY-MM-DD");
const end = (debouncedUserFilter.to ?? dayjs()).format("YYYY-MM-DD");
const transactions = await getDateRangedTransactions(state.server, start, end, state.profile);
setExistingTransactions(transactions);
}, [enabledExistingTransactions, state.server, debouncedUserFilter]);
useEffect(() => {
setDeselected([])
}, [filteredTransactions]);
useEffect(() => {
refreshExistingTransactions()
}, [enabledExistingTransactions, state.server, debouncedUserFilter]);
return (
<div className="columns mt-6">
<div className="column is-8 is-offset-2">
<div className="box">
<h2 className="title is-4 has-text-centered">Prepare Transactions</h2>
<div className="content">
<h3 className="title is-5">Filters</h3>
<FilterPanel value={userFilter} setValue={setUserFilter} />
</div>
<div className="field">
<label className="label">Fetch existing transactions</label>
<div className="notification is-info is-light">
If enabled, the existing transactions will be fetched automatically basing on date filter.
If "from" is filled, the existing transactions will be fetched basing on set date (if "to" is not filled, the todays date will be considered).
Otherwise, the latest <abbr title="Transaction before transfer consolidation.">10 raw transactions</abbr> will be fetched. Because of transfer consolidation,
the ultimate number of transactions can be lower.
</div>
<div className="control">
<div className="buttons has-addons">
<button
className={classNames('button', { 'is-selected': enabledExistingTransactions, 'is-success': enabledExistingTransactions})}
onClick={() => setEnabledExistingTransactions(true)}
disabled={enabledExistingTransactions}>I</button>
<button
className={classNames('button', { 'is-selected': !enabledExistingTransactions, 'is-danger': !enabledExistingTransactions})}
onClick={() => setEnabledExistingTransactions(false)}
disabled={!enabledExistingTransactions}>O</button>
</div>
</div>
</div>
<div className="columns">
<div className={classNames('column', { 'is-6': enabledExistingTransactions, 'is-12': !enabledExistingTransactions })}>
<h3 className="title is-5">Considered transactions ({selectedTransactions.length}/{filteredTransactions.length})</h3>
<TransactionsTable
transactions={filteredTransactions}
deselected={deselected}
deselect={deselect}
select={select} />
</div>
{enabledExistingTransactions &&
<div className="column is-6">
<h3 className="title is-5">Existing transactions ({existingTransactions.length})</h3>
<ExistingTransactions
loading={loadingExistingTransactions}
transactions={existingTransactions} />
</div>}
</div>
<div className="content">
<h3 className="title is-5">Skipped lines ({state.skipped.length})</h3>
<SkippedLines skipped={state.skipped} />
</div>
<div className="content">
<ImportBar loading={loading} onSubmit={handleSubmit} onStartOver={() => location.reload()}/>
</div>
</div>
</div>
</div>
);
}