Create working PoC of frontend app
This commit is contained in:
77
web/src/pages/PrepareTransactionsPage/ImportBar.tsx
Normal file
77
web/src/pages/PrepareTransactionsPage/ImportBar.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import type { ImportOptions } from "../../types/api";
|
||||
|
||||
export type ImportBarProps = {
|
||||
onSubmit: (opts: ImportOptions) => void;
|
||||
};
|
||||
|
||||
export default function ImportBar({ onSubmit }: ImportBarProps) {
|
||||
const [formData, setFormData] = useState<ImportOptions>({ mode: 'import' });
|
||||
|
||||
|
||||
const handleSubmit = useCallback((e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
|
||||
onSubmit(formData);
|
||||
}, [formData, onSubmit]);
|
||||
|
||||
const changeMode = useCallback((mode: ImportOptions['mode']) => {
|
||||
if (mode === 'add') {
|
||||
setFormData({ ...formData, mode });
|
||||
}
|
||||
|
||||
else {
|
||||
setFormData({ mode });
|
||||
}
|
||||
}, [formData, setFormData]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="field">
|
||||
<label className="label">Import mode</label>
|
||||
<div className="control">
|
||||
<div className="buttons has-addons is-left">
|
||||
<button
|
||||
type="button"
|
||||
className={`button ${formData.mode === 'import' ? "is-primary is-selected" : ""}`}
|
||||
onClick={() => changeMode('import')}>Import</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`button ${formData.mode === 'add' ? "is-primary is-selected" : ""}`}
|
||||
onClick={() => changeMode('add')}>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.mode === 'add' && <div className="field">
|
||||
<label className="label">Additional options</label>
|
||||
<div className="control">
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.learnCategories}
|
||||
onChange={e => setFormData({ ...formData, learnCategories: e.target.checked})}/> Learn categories
|
||||
</label>
|
||||
</div>
|
||||
<div className="control">
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.runTransfers}
|
||||
onChange={e => setFormData({ ...formData, runTransfers: e.target.checked})}/> Run transfers
|
||||
</label>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<div className="field mt-5">
|
||||
<div className="control">
|
||||
<button
|
||||
type="submit"
|
||||
className="button is-link is-fullwidth">Submit transactions</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useStore } from "../../store/AppStore";
|
||||
import TransactionsTable from "./TransactionsTable";
|
||||
import { SkippedLines } from "./SkippedLines";
|
||||
import ImportBar from "./ImportBar";
|
||||
import type { ImportOptions } from "../../types/api";
|
||||
import { submitTransactions } from "../../services/api.service";
|
||||
|
||||
|
||||
export default function PrepareTransactionsPage() {
|
||||
const { state, dispatch } = useStore();
|
||||
|
||||
const desiredTransactions = useMemo(() => state.transactions.filter((_, i) => !state.filteredOut.includes(i)), [state, state.filteredOut]);
|
||||
|
||||
const handleSubmit = useCallback(async (opts: ImportOptions) => {
|
||||
if(!state.server) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await submitTransactions(desiredTransactions, state.server, opts);
|
||||
|
||||
dispatch({
|
||||
type: 'SET_RESULT',
|
||||
payload: response
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: 'MOVE_STEP',
|
||||
payload: {
|
||||
offset: 1
|
||||
}
|
||||
});
|
||||
}, [desiredTransactions, state]);
|
||||
|
||||
const filterOut = useCallback((index: number) => dispatch({
|
||||
type: 'FILTER_OUT',
|
||||
payload: { index }
|
||||
}), [dispatch]);
|
||||
|
||||
const removeFilter = useCallback((index: number) => dispatch({
|
||||
type: 'REMOVE_FILTER',
|
||||
payload: { index }
|
||||
}), [dispatch]);
|
||||
|
||||
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">Considered transactions ({state.transactions.length})</h3>
|
||||
<TransactionsTable
|
||||
transactions={state.transactions}
|
||||
filteredOut={state.filteredOut}
|
||||
filterOut={filterOut}
|
||||
removeFilter={removeFilter} />
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
<h3 className="title is-5">Skipped lines ({state.skipped.length})</h3>
|
||||
<SkippedLines skipped={state.skipped} />
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
<ImportBar onSubmit={handleSubmit} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.skipped {
|
||||
content: red;
|
||||
}
|
||||
19
web/src/pages/PrepareTransactionsPage/SkippedLines.tsx
Normal file
19
web/src/pages/PrepareTransactionsPage/SkippedLines.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import styles from "./SkippedLines.module.css";
|
||||
|
||||
export type SkippedLinesProps = {
|
||||
skipped: string[][];
|
||||
};
|
||||
|
||||
export function SkippedLines({ skipped }: SkippedLinesProps) {
|
||||
if (skipped.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.skipped}>
|
||||
<pre>
|
||||
{skipped.map(s => `▶️ ${s.join("🔸")}`).join("\n")}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.transactionTable tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled td {
|
||||
color: var(--bulma-text-light);
|
||||
}
|
||||
63
web/src/pages/PrepareTransactionsPage/TransactionsTable.tsx
Normal file
63
web/src/pages/PrepareTransactionsPage/TransactionsTable.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useCallback } from "react";
|
||||
import type { Transaction } from "../../types/api";
|
||||
import styles from "./TransactionTable.module.css";
|
||||
|
||||
export type TransactionsTableProps = {
|
||||
transactions: Transaction[];
|
||||
filteredOut: number[];
|
||||
filterOut: (index: number) => void;
|
||||
removeFilter: (index: number) => void;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export default function TransactionsTable({ transactions, filteredOut, filterOut, removeFilter, readonly }: TransactionsTableProps) {
|
||||
|
||||
const changeSelection = useCallback((index: number) => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(filteredOut.includes(index)) {
|
||||
removeFilter(index);
|
||||
}
|
||||
|
||||
else {
|
||||
filterOut(index);
|
||||
}
|
||||
}, [filteredOut, filterOut, removeFilter]);
|
||||
|
||||
|
||||
const renderRow = useCallback((transaction: Transaction, index: number) => (
|
||||
<tr key={index} onClick={() => changeSelection(index)} className={`${!readonly && filteredOut.includes(index) ? styles.disabled : ""}`}>
|
||||
<td>{transaction.kind}</td>
|
||||
<td>{transaction.date}</td>
|
||||
<td>{transaction.from}</td>
|
||||
<td>{transaction.to}</td>
|
||||
<td>{transaction.amount}</td>
|
||||
<td>{transaction.title}</td>
|
||||
<td>{transaction.id}</td>
|
||||
</tr>
|
||||
), [changeSelection]);
|
||||
|
||||
return (
|
||||
<div className="table-container">
|
||||
<table className={`table is-hoverable ${!readonly ? styles.transactionTable : ""}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
<th>Kind</th>
|
||||
<th>Date</th>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>Amount</th>
|
||||
<th>Title</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map(renderRow)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user