Skip to content

Commit

Permalink
feat: commodity autocomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
kajyr committed Feb 8, 2022
1 parent 0271b1a commit 35d2720
Show file tree
Hide file tree
Showing 8 changed files with 6,989 additions and 7,315 deletions.
12,404 changes: 6,065 additions & 6,339 deletions backend/package-lock.json

Large diffs are not rendered by default.

45 changes: 28 additions & 17 deletions backend/suggestions/get-sorted-accounts-matching-descr.test.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,43 @@
import { Transaction } from 'pta-tools';
import { Transaction } from "pta-tools";

import getSortedAccountsMatchingDescr from './get-sorted-accounts-matching-descr';
import getSortedAccountsMatchingDescr from "./get-sorted-accounts-matching-descr";

function mockTransaction(account, description?): Transaction {
function mockTransaction(account, description, commodity): Transaction {
return {
description,
date: new Date("2020-01-01"),
entries: [{ account, amount: 3 }, { account: "Assets:Cash" }],
entries: [{ account, amount: 3, commodity }, { 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"),
mockTransaction("Expenses:Bananas", "Supermarket", "EUR"),
mockTransaction("Expenses:Breakfast", "Bar", "EUR"),
mockTransaction("Expenses:Bananas", "Supermarket", "EUR"),
mockTransaction("Expenses:Bananas", "Supermarket", "USD"),
mockTransaction("Expenses:Bananas", "Supermarket", "USD"),
mockTransaction("Expenses:Breakfast", "cafeteria", "EUR"),
mockTransaction("Expenses:Lunch", "bar", "USD"),
mockTransaction("Assets:Bank", undefined, "EUR"),
mockTransaction("Expenses:Groceries", "Supermarket", "EUR"),
mockTransaction("Expenses:Groceries", "Supermarket", "ETH"),
];

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)
getSortedAccountsMatchingDescr(journal, "b", "bar", "account")
).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)
getSortedAccountsMatchingDescr(journal, undefined, "bar", "account")
).toEqual([
"Assets:Cash", // This is used in every mock, so first
"Expenses:Breakfast",
Expand All @@ -52,7 +52,9 @@ describe("getSortedAccountsMatchingDescr", () => {
// Breakfast is first because it matches bar
// even if it occurres less than Bananas
expect(
getSortedAccountsMatchingDescr(journal, undefined, "bar", ["expenses"])
getSortedAccountsMatchingDescr(journal, undefined, "bar", "account", [
"expenses",
])
).toEqual([
"Expenses:Breakfast",
"Expenses:Lunch", // this is used less, but matches bar
Expand All @@ -62,4 +64,13 @@ describe("getSortedAccountsMatchingDescr", () => {
"Assets:Bank",
]);
});

test("Extracts commodities", () => {
// 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, "e", "bar", "commodity")
).toEqual(["EUR", "ETH"]);
});
});
27 changes: 14 additions & 13 deletions backend/suggestions/get-sorted-accounts-matching-descr.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isPosting, isTransaction, Journal, Transaction } from 'pta-tools';
import { isPosting, isTransaction, Journal, Transaction } from "pta-tools";

type Pivot = {
account: string;
Expand All @@ -7,11 +7,11 @@ type Pivot = {
uses: number;
};

function getAccounts(trx: Transaction) {
function getKeysFromPostings(trx: Transaction, filterKey: string): string[] {
const accounts: string[] = [];
for (const entry of trx.entries) {
if (isPosting(entry)) {
accounts.push(entry.account);
if (isPosting(entry) && entry[filterKey]) {
accounts.push(entry[filterKey]);
}
}
return accounts;
Expand All @@ -32,6 +32,7 @@ function getSortedAccountsMatchingDescr(
journal: Journal,
query: string | undefined,
description: string | undefined,
filterKey: string,
sort?: string[]
): string[] {
const descr = description?.toLowerCase();
Expand All @@ -47,17 +48,17 @@ function getSortedAccountsMatchingDescr(
const descrMatch =
descr && trx.description?.toLowerCase().startsWith(descr) ? 1 : 0;

for (const account of getAccounts(trx)) {
const accName = account.toLowerCase();
const sortMatch = startsWith(accName, s);
for (const key of getKeysFromPostings(trx, filterKey)) {
const keyLower = key.toLowerCase();
const sortMatch = startsWith(keyLower, s);

if (!q || accName.includes(q)) {
if (!map[account]) {
map[account] = { account, descrMatch, sortMatch, uses: 1 };
if (!q || keyLower.includes(q)) {
if (!map[key]) {
map[key] = { account: key, descrMatch, sortMatch, uses: 1 };
} else {
map[account].descrMatch += descrMatch;
map[account].sortMatch = map[account].sortMatch || sortMatch;
map[account].uses++;
map[key].descrMatch += descrMatch;
map[key].sortMatch = map[key].sortMatch || sortMatch;
map[key].uses++;
}
}
}
Expand Down
22 changes: 16 additions & 6 deletions backend/suggestions/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { isTransaction, Journal } from 'pta-tools';
import { isTransaction, Journal } from "pta-tools";

import { readFile } from '../dal';
import { sortByOccurrence } from '../helpers/array';
import { readFile } from "../dal";
import { sortByOccurrence } from "../helpers/array";

import getSortedAccountsMatchingDescr from './get-sorted-accounts-matching-descr';
import getSortedAccountsMatchingDescr from "./get-sorted-accounts-matching-descr";

function getPayees(trxs: Journal): string[] {
return trxs.reduce((acc, trx) => {
Expand All @@ -14,6 +14,15 @@ function getPayees(trxs: Journal): string[] {
}, [] as string[]);
}

function getCommodities(trxs: Journal): string[] {
return trxs.reduce((acc, trx) => {
if (isTransaction(trx) && trx.description) {
acc.push(trx.description);
}
return acc;
}, [] as string[]);
}

export default function (fastify, opts, done) {
const routes = [
{
Expand Down Expand Up @@ -45,17 +54,18 @@ export default function (fastify, opts, done) {
},
{
method: "GET",
url: `/api/s/account/:query?`,
url: `/api/s/:entity/:query?`,
handler: async function (request) {
const data = await readFile();

const { query } = request.params;
const { query, entity } = request.params;
const { description, sort } = request.query;

const list = getSortedAccountsMatchingDescr(
data.journal,
query,
description,
entity,
sort?.split(",")
);

Expand Down
2 changes: 1 addition & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 16 additions & 15 deletions frontend/src/pages/dashboard/entry-row.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import React, { FC } from 'react';
import React, { FC } from "react";

import AsyncAutocomplete from 'atoms/async-autocomplete';
import AsyncAutocomplete from "atoms/async-autocomplete";

import { Autocomplete, Button, createStyles, Group, Space, TextInput } from '@mantine/core';
import {
Autocomplete,
Button,
createStyles,
Group,
Space,
TextInput,
} from "@mantine/core";

import { Comment, isComment, Posting } from 'pta-tools';
import { Comment, isComment, Posting } from "pta-tools";

const useStyles = createStyles((theme) => {
return {
Expand All @@ -24,18 +31,14 @@ const useStyles = createStyles((theme) => {
const EntryRow: FC<{
amountPlaceholder: string | null;
canDelete: boolean;
commodities: string[];
description: string | undefined;
entry: Posting | Comment;
removeRow: () => void;
suggestedCommodity: string | undefined;
updateRow: (field: string, value: string) => void;
}> = ({
amountPlaceholder,
canDelete,
commodities,
entry,
suggestedCommodity,
removeRow,
updateRow,
description,
Expand Down Expand Up @@ -66,15 +69,13 @@ const EntryRow: FC<{
style={{ flex: 2 }}
onChange={(event) => updateRow("amount", event.currentTarget.value)}
/>
<Autocomplete
placeholder={suggestedCommodity || "Commodity"}
value={entry.commodity}
<AsyncAutocomplete
endpoint="/api/s/commodity"
params={params.join("&")}
placeholder="Commodity"
value={entry.commodity || ""}
style={{ flex: 1 }}
onChange={(value) => updateRow("commodity", value)}
data={commodities}
filter={(value, item) =>
item.value.toLowerCase().includes(value.toLowerCase().trim())
}
/>
{canDelete ? (
<Button compact onClick={removeRow} variant="outline">
Expand Down
54 changes: 29 additions & 25 deletions frontend/src/pages/dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
import React, { FC, useState } from 'react';

import { callApi } from 'helpers/api';
import { isToday } from 'helpers/dates';

import AsyncAutocomplete from 'atoms/async-autocomplete';

import { Button, Chip, Chips, Group, LoadingOverlay, Paper, Popover, Text, Title } from '@mantine/core';
import { Calendar } from '@mantine/dates';
import { useForm } from '@mantine/hooks';
import { useNotifications } from '@mantine/notifications';

import { Posting, Transaction } from 'pta-tools';
import { Api } from 'types';

import ConfirmationModal from './confirmation-modal';
import EntryRow from './entry-row';
import PaymentAccount from './payment-acct-row';
import prepareSubmitData from './prepare-submit';
import React, { FC, useState } from "react";

import { callApi } from "helpers/api";
import { isToday } from "helpers/dates";

import AsyncAutocomplete from "atoms/async-autocomplete";

import {
Button,
Chip,
Chips,
Group,
LoadingOverlay,
Paper,
Popover,
Text,
Title,
} from "@mantine/core";
import { Calendar } from "@mantine/dates";
import { useForm } from "@mantine/hooks";
import { useNotifications } from "@mantine/notifications";

import { Posting, Transaction } from "pta-tools";
import { Api } from "types";

import ConfirmationModal from "./confirmation-modal";
import EntryRow from "./entry-row";
import PaymentAccount from "./payment-acct-row";
import prepareSubmitData from "./prepare-submit";

const EMPTY_ENTRY: Posting = {
account: "",
Expand Down Expand Up @@ -121,10 +131,6 @@ const Dashboard: FC<{ journal: Api.BootstrapResponse }> = ({ journal }) => {
? (outBalance * -1).toString()
: null;

const singleCommodity = allValuesAreEqual(
values.entries.map((e) => (e as Posting).commodity)
);

const dateStr = isToday(new Date(values.date))
? "Today"
: values.date.toLocaleDateString();
Expand Down Expand Up @@ -179,13 +185,11 @@ const Dashboard: FC<{ journal: Api.BootstrapResponse }> = ({ journal }) => {
<EntryRow
description={values.description}
canDelete={i !== 0}
commodities={journal.commodities}
entry={entry}
key={i}
removeRow={removeRow(i)}
updateRow={updateRow(i)}
amountPlaceholder={amountPlaceholder}
suggestedCommodity={singleCommodity}
/>
))}
<PaymentAccount
Expand Down
Loading

0 comments on commit 35d2720

Please sign in to comment.