diff --git a/web/package-lock.json b/web/package-lock.json index 4c88c2e..7b3d381 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "bulma": "^1.0.4", + "dayjs": "^1.11.13", "react": "^19.1.0", "react-dom": "^19.1.0" }, @@ -3695,6 +3696,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/web/package.json b/web/package.json index 1892eca..a4c20ce 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "bulma": "^1.0.4", + "dayjs": "^1.11.13", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/web/package.nix b/web/package.nix index 9e19b90..da2021a 100644 --- a/web/package.nix +++ b/web/package.nix @@ -7,5 +7,5 @@ buildNpmPackage { pname = "actual-importer-frontend"; version = "0.0.1"; src = ./.; - npmDepsHash = "sha256-oNs/nnATxuR8mrIRznzIBk5/gWIR+Ie/i8PD2HhYYFA="; + npmDepsHash = "sha256-/G8OZBAkCuc0LTmpB1v8HTgaWdYd9AJfZTC0/eUQIx0="; } diff --git a/web/src/pages/PrepareTransactionsPage/FilterPanel.tsx b/web/src/pages/PrepareTransactionsPage/FilterPanel.tsx new file mode 100644 index 0000000..8d523dd --- /dev/null +++ b/web/src/pages/PrepareTransactionsPage/FilterPanel.tsx @@ -0,0 +1,121 @@ +import type { Dayjs } from "dayjs"; +import dayjs from "dayjs"; +import { useCallback } from "react"; + +export type FilterData = { + from?: Dayjs; + to?: Dayjs; +}; + +export type FilterPanelProps = { + value?: FilterData; + setValue: (value: FilterData) => void; +}; + +const str2dayjs = (text?: string) => text ? dayjs(text) : undefined; +const dayjs2str = (date?: Dayjs) => !date ? "" : date.format("YYYY-MM-DD"); + +export function FilterPanel({ value, setValue }: FilterPanelProps) { + const handleChange = useCallback((k: K, v: FilterData[K]) => { + setValue({ ...value, [k]: v }); + }, [value]); + + const handleQuickDate = useCallback((key: 'from'|'to', days: number) => { + const date = dayjs().subtract(days, 'days'); + + setValue({ + ...value, + [key]: date + }); + }, [value, setValue]); + + return (<> +
+
+ +
+
+
+
+ handleChange('from', str2dayjs(e.target.value))} + /> +

From (transactions not earlier than...)

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ handleChange('to', str2dayjs(e.target.value))} + /> +

To (transactions not later than...)

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/web/src/pages/PrepareTransactionsPage/PrepareTransactionsPage.tsx b/web/src/pages/PrepareTransactionsPage/PrepareTransactionsPage.tsx index cb0a3c8..6a9b854 100644 --- a/web/src/pages/PrepareTransactionsPage/PrepareTransactionsPage.tsx +++ b/web/src/pages/PrepareTransactionsPage/PrepareTransactionsPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useStore } from "../../store/AppStore"; import TransactionsTable from "./TransactionsTable"; import { SkippedLines } from "./SkippedLines"; @@ -6,19 +6,48 @@ import ImportBar from "./ImportBar"; import type { ImportOptions } from "../../types/api"; import { submitTransactions } from "../../services/api.service"; import { useLoader } from "../../hooks/useLoader"; +import { FilterPanel, type FilterData } from "./FilterPanel"; +import dayjs from "dayjs"; export default function PrepareTransactionsPage() { const { state, dispatch } = useStore(); + + const [userFilter, setUserFilter] = useState({}); + const [deselected, setDeselected] = useState([]); + + const select = useCallback((index: number) => setDeselected(deselected.filter(x => x !== index)), [deselected, setDeselected]); + const deselect = useCallback((index: number) => setDeselected([...deselected, index]), [deselected, setDeselected]); - const desiredTransactions = useMemo(() => state.transactions.filter((_, i) => !state.filteredOut.includes(i)), [state, state.filteredOut]); + 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'); + }) + , [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(desiredTransactions, state.server, opts); + const response = await submitTransactions(selectedTransactions, state.server, opts); dispatch({ type: 'SET_RESULT', @@ -31,17 +60,9 @@ export default function PrepareTransactionsPage() { offset: 1 } }); - }, [desiredTransactions, state]); + }, [selectedTransactions, 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]); + useEffect(() => setDeselected([]), [filteredTransactions]); return (
@@ -50,12 +71,17 @@ export default function PrepareTransactionsPage() {

Prepare Transactions

-

Considered transactions ({state.transactions.length-state.filteredOut.length}/{state.transactions.length})

+

Filters

+ +
+ +
+

Considered transactions ({selectedTransactions.length}/{filteredTransactions.length})

+ transactions={filteredTransactions} + deselected={deselected} + deselect={deselect} + select={select} />
diff --git a/web/src/pages/PrepareTransactionsPage/TransactionsTable.tsx b/web/src/pages/PrepareTransactionsPage/TransactionsTable.tsx index 2c2bc7c..31cd02e 100644 --- a/web/src/pages/PrepareTransactionsPage/TransactionsTable.tsx +++ b/web/src/pages/PrepareTransactionsPage/TransactionsTable.tsx @@ -4,31 +4,31 @@ import styles from "./TransactionTable.module.css"; export type TransactionsTableProps = { transactions: Transaction[]; - filteredOut: number[]; - filterOut: (index: number) => void; - removeFilter: (index: number) => void; + deselected: number[]; + deselect: (index: number) => void; + select: (index: number) => void; readonly?: boolean; } -export default function TransactionsTable({ transactions, filteredOut, filterOut, removeFilter, readonly }: TransactionsTableProps) { +export default function TransactionsTable({ transactions, deselected, deselect, select, readonly }: TransactionsTableProps) { const changeSelection = useCallback((index: number) => { if (readonly) { return; } - if(filteredOut.includes(index)) { - removeFilter(index); + if(deselected.includes(index)) { + select(index); } else { - filterOut(index); + deselect(index); } - }, [filteredOut, filterOut, removeFilter]); + }, [deselected, deselect, select]); const renderRow = useCallback((transaction: Transaction, index: number) => ( - changeSelection(index)} className={`${!readonly && filteredOut.includes(index) ? styles.disabled : ""}`}> + changeSelection(index)} className={`${!readonly && deselected.includes(index) ? styles.disabled : ""}`}> {transaction.kind} {transaction.date} {transaction.from} diff --git a/web/src/store/reducer.ts b/web/src/store/reducer.ts index b28484e..5a79c9e 100644 --- a/web/src/store/reducer.ts +++ b/web/src/store/reducer.ts @@ -38,18 +38,6 @@ export function reducer(state: State, action: Action): State { } } - case 'FILTER_OUT': - return { - ...state, - filteredOut: [...state.filteredOut, action.payload.index] - } - - case 'REMOVE_FILTER': - return { - ...state, - filteredOut: state.filteredOut.filter(x => x !== action.payload.index) - } - case 'SET_RESULT': return { ...state, diff --git a/web/src/store/state.ts b/web/src/store/state.ts index 4e6af4b..3d830d4 100644 --- a/web/src/store/state.ts +++ b/web/src/store/state.ts @@ -9,6 +9,4 @@ export const initialState: State = { wizard: { step: 0 }, - - filteredOut: [] }; \ No newline at end of file diff --git a/web/src/types/store.ts b/web/src/types/store.ts index b8b7391..b19f235 100644 --- a/web/src/types/store.ts +++ b/web/src/types/store.ts @@ -14,8 +14,6 @@ export type State = { step: number; }; - filteredOut: number[]; - result?: { errors?: { message: string; @@ -57,18 +55,6 @@ export type Action = offset: number; } } -| { - type: 'FILTER_OUT', - payload: { - index: number; - }; -} -| { - type: 'REMOVE_FILTER', - payload: { - index: number; - }; -} | { type: 'SET_RESULT', payload: {