167 lines
6.6 KiB
TypeScript
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>
|
|
);
|
|
} |