-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Account suggestions now considers description to provide better…
… suggestions
- Loading branch information
Showing
6 changed files
with
192 additions
and
18 deletions.
There are no files selected for viewing
65 changes: 65 additions & 0 deletions
65
backend/suggestions/get-sorted-accounts-matching-descr.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { Transaction } from 'pta-tools'; | ||
|
||
import getSortedAccountsMatchingDescr from './get-sorted-accounts-matching-descr'; | ||
|
||
function mockTransaction(account, description?): Transaction { | ||
return { | ||
description, | ||
date: new Date("2020-01-01"), | ||
entries: [{ account, amount: 3 }, { account: "Assets:Cash" }], | ||
}; | ||
} | ||
|
||
describe("getSortedAccountsMatchingDescr", () => { | ||
const journal = [ | ||
mockTransaction("Expenses:Bananas", "Supermarket"), | ||
mockTransaction("Expenses:Breakfast", "Bar"), | ||
mockTransaction("Expenses:Bananas", "Supermarket"), | ||
mockTransaction("Expenses:Bananas", "Supermarket"), | ||
mockTransaction("Expenses:Bananas", "Supermarket"), | ||
mockTransaction("Expenses:Breakfast", "cafeteria"), | ||
mockTransaction("Expenses:Lunch", "bar"), | ||
mockTransaction("Assets:Bank"), | ||
mockTransaction("Expenses:Groceries", "Supermarket"), | ||
mockTransaction("Expenses:Groceries", "Supermarket"), | ||
]; | ||
|
||
test("Returns sorted account list", () => { | ||
// Breakfast is first because it matches bar | ||
// even if it occurres less than Bananas | ||
// Bananas goes before Bank because it is used more often | ||
expect( | ||
getSortedAccountsMatchingDescr(journal, "b", "bar", undefined) | ||
).toEqual(["Expenses:Breakfast", "Expenses:Bananas", "Assets:Bank"]); | ||
}); | ||
|
||
test("With no query, just sort by descr", () => { | ||
// Breakfast is first because it matches bar | ||
// even if it occurres less than Bananas | ||
expect( | ||
getSortedAccountsMatchingDescr(journal, undefined, "bar", undefined) | ||
).toEqual([ | ||
"Assets:Cash", // This is used in every mock, so first | ||
"Expenses:Breakfast", | ||
"Expenses:Lunch", // this is used less, but matches bar | ||
"Expenses:Bananas", // this is used more, but does not match descr | ||
"Expenses:Groceries", | ||
"Assets:Bank", | ||
]); | ||
}); | ||
|
||
test("With no query, just sort by descr AND sorting key", () => { | ||
// Breakfast is first because it matches bar | ||
// even if it occurres less than Bananas | ||
expect( | ||
getSortedAccountsMatchingDescr(journal, undefined, "bar", "expenses") | ||
).toEqual([ | ||
"Expenses:Breakfast", | ||
"Expenses:Lunch", // this is used less, but matches bar | ||
"Expenses:Bananas", // this is used more, but does not match descr | ||
"Expenses:Groceries", | ||
"Assets:Cash", // This is used in every mock, but does not match sorting | ||
"Assets:Bank", | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { isPosting, isTransaction, Journal, Transaction } from 'pta-tools'; | ||
|
||
type Pivot = { | ||
account: string; | ||
descrMatch: number; | ||
sortMatch: boolean; | ||
uses: number; | ||
}; | ||
|
||
function getAccounts(trx: Transaction) { | ||
const accounts: string[] = []; | ||
for (const entry of trx.entries) { | ||
if (isPosting(entry)) { | ||
accounts.push(entry.account); | ||
} | ||
} | ||
return accounts; | ||
} | ||
|
||
function getSortedAccountsMatchingDescr( | ||
journal: Journal, | ||
query: string | undefined, | ||
description: string | undefined, | ||
sort: string | undefined | ||
): string[] { | ||
const descr = description?.toLowerCase(); | ||
const q = query?.toLowerCase(); | ||
const s = sort?.toLowerCase(); | ||
|
||
const map = {} as Record<string, Pivot>; | ||
|
||
for (const trx of journal) { | ||
if (!isTransaction(trx)) { | ||
continue; | ||
} | ||
const descrMatch = | ||
descr && trx.description?.toLowerCase().startsWith(descr) ? 1 : 0; | ||
|
||
for (const account of getAccounts(trx)) { | ||
const accName = account.toLowerCase(); | ||
const sortMatch = !!s && accName.startsWith(s); | ||
|
||
if (!q || accName.includes(q)) { | ||
if (!map[account]) { | ||
map[account] = { account, descrMatch, sortMatch, uses: 1 }; | ||
} else { | ||
map[account].descrMatch += descrMatch; | ||
map[account].sortMatch = map[account].sortMatch || sortMatch; | ||
map[account].uses++; | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Sort by sorting matching | ||
// Then by descr matching | ||
// Then by usage | ||
// Then by name | ||
const sorted = Object.values(map).sort( | ||
(a, b) => | ||
Number(b.sortMatch) - Number(a.sortMatch) || | ||
b.descrMatch - a.descrMatch || | ||
b.uses - a.uses || | ||
a.account.localeCompare(b.account) | ||
); | ||
|
||
return sorted.map((v) => v.account); | ||
} | ||
|
||
export default getSortedAccountsMatchingDescr; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,34 @@ | ||
import React, { FC } from 'react'; | ||
import React, { FC, useState } from 'react'; | ||
import { useQuery } from 'react-query'; | ||
|
||
import { Autocomplete, AutocompleteProps } from '@mantine/core'; | ||
|
||
type Props = Omit<AutocompleteProps, "data"> & { endpoint: string }; | ||
type Props = Omit<AutocompleteProps, "data"> & { | ||
endpoint: string; | ||
params?: string; | ||
}; | ||
|
||
type Suggestions = string[]; | ||
|
||
const AsyncAutocomplete: FC<Props> = ({ endpoint, ...props }) => { | ||
const { isLoading, error, data } = useQuery<Suggestions>( | ||
[endpoint, props.value], | ||
const AsyncAutocomplete: FC<Props> = ({ endpoint, params, ...props }) => { | ||
const [isOpen, setOpen] = useState(false); | ||
const { data } = useQuery<Suggestions>( | ||
[endpoint, props.value, params], | ||
() => { | ||
const val = props.value || ""; | ||
return fetch(`${endpoint}/${props.value}`).then((res) => res.json()); | ||
|
||
return Promise.resolve([]); | ||
return fetch( | ||
`${endpoint}/${props.value}${params ? `?${params}` : ""}` | ||
).then((res) => res.json()); | ||
}, | ||
{ retry: false } | ||
{ retry: false, enabled: isOpen } | ||
); | ||
return ( | ||
<Autocomplete | ||
{...props} | ||
data={data || []} | ||
onFocus={() => setOpen(true)} | ||
onBlur={() => setOpen(false)} | ||
/> | ||
); | ||
|
||
return <Autocomplete {...props} data={data || []} />; | ||
}; | ||
|
||
export default AsyncAutocomplete; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters