diff --git a/.gitignore b/.gitignore index bc7739a8..759c67be 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ mgo coverage.cov money.db .DS_Store +__debug* +moneyd diff --git a/gen/portfolio.go b/gen/portfolio.go index 81789227..394f4606 100644 --- a/gen/portfolio.go +++ b/gen/portfolio.go @@ -2,9 +2,9 @@ package portfoliov1 import ( "hash/fnv" + "log/slog" "strconv" "time" - "log/slog" ) func (p *Portfolio) EventMap() (m map[string][]*PortfolioEvent) { @@ -50,10 +50,22 @@ func (tx *PortfolioEvent) MakeUniqueName() { tx.Name = strconv.FormatUint(h.Sum64(), 16) } +// LogValue implements slog.LogValuer. +func (tx *PortfolioEvent) LogValue() slog.Value { + return slog.GroupValue( + slog.String("name", tx.Name), + slog.String("security.name", tx.SecurityName), + slog.Float64("amount", float64(tx.Amount)), + slog.Float64("price", float64(tx.Price)), + slog.Float64("fees", float64(tx.Fees)), + slog.Float64("taxes", float64(tx.Taxes)), + ) +} + // LogValue implements slog.LogValuer. func (ls *ListedSecurity) LogValue() slog.Value { return slog.GroupValue( slog.String("name", ls.SecurityName), slog.String("ticker", ls.Ticker), ) -} \ No newline at end of file +} diff --git a/money-gopher.code-workspace b/money-gopher.code-workspace index 509c6610..fa883ead 100644 --- a/money-gopher.code-workspace +++ b/money-gopher.code-workspace @@ -33,6 +33,7 @@ "portfoliov", "protobuf", "protoreflect", + "rgossiaux", "secmap", "secres", "steeze", diff --git a/service/portfolio/transactions.go b/service/portfolio/transactions.go index de8d1b58..98d56f60 100644 --- a/service/portfolio/transactions.go +++ b/service/portfolio/transactions.go @@ -19,6 +19,7 @@ package portfolio import ( "bytes" "context" + "log/slog" portfoliov1 "github.com/oxisto/money-gopher/gen" "github.com/oxisto/money-gopher/import/csv" @@ -36,6 +37,8 @@ func (svc *service) CreatePortfolioTransaction(ctx context.Context, req *connect // Create a unique name for the transaction req.Msg.Transaction.MakeUniqueName() + slog.Info("Creating transaction", "transaction", req.Msg.Transaction) + return crud.Create( req.Msg.Transaction, svc.events, diff --git a/ui/package-lock.json b/ui/package-lock.json index 6b450dae..b56d4a42 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,7 +8,8 @@ "name": "ui", "version": "0.0.1", "devDependencies": { - "@bufbuild/connect-web": "^0.13.0", + "@connectrpc/connect": "^1.1.3", + "@connectrpc/connect-web": "^1.1.3", "@steeze-ui/heroicons": "^2.2.3", "@steeze-ui/svelte-icon": "^1.5.0", "@sveltejs/adapter-auto": "^2.0.0", @@ -70,33 +71,31 @@ "node": ">=6.0.0" } }, - "node_modules/@bufbuild/connect": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@bufbuild/connect/-/connect-0.13.0.tgz", - "integrity": "sha512-eZSMbVLyUFtXiZNORgCEvv580xKZeYQdMOWj2i/nxOcpXQcrEzTMTA7SZzWv4k4gveWCOSRoWmYDeOhfWXJv0g==", + "node_modules/@bufbuild/protobuf": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.6.0.tgz", + "integrity": "sha512-hp19vSFgNw3wBBcVBx5qo5pufCqjaJ0Cfk5H/pfjNOfNWU+4/w0QVOmfAOZNRrNWRrVuaJWxcN8P2vhOkkzbBQ==", "dev": true, - "peerDependencies": { - "@bufbuild/protobuf": "^1.2.1" - } + "peer": true }, - "node_modules/@bufbuild/connect-web": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@bufbuild/connect-web/-/connect-web-0.13.0.tgz", - "integrity": "sha512-Ys9VFDWYktD9yFQSLOlkpsD42LonDNMCysLCfjXFuxlupYuf4f7qg0zkT5bESyTfqk4xtRDSSGR3xygaj/ONIQ==", + "node_modules/@connectrpc/connect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.2.0.tgz", + "integrity": "sha512-kHF30xAlXF2Y7S1I7XN/D3psKLfjxitgNRmF093KNP+cE9yAnqDAGop6aby3Z5k4XQw2ebjeX4E41db7R3FzaQ==", "dev": true, - "dependencies": { - "@bufbuild/connect": "0.13.0" - }, "peerDependencies": { - "@bufbuild/protobuf": "^1.2.1" + "@bufbuild/protobuf": "^1.4.2" } }, - "node_modules/@bufbuild/protobuf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.3.0.tgz", - "integrity": "sha512-G372ods0pLt46yxVRsnP/e2btVPuuzArcMPFpIDeIwiGPuuglEs9y75iG0HMvZgncsj5TvbYRWqbVyOe3PLCWQ==", + "node_modules/@connectrpc/connect-web": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.2.0.tgz", + "integrity": "sha512-vjFKTP/AzSnC8JvkGKRgpggZIB0v+Lv7U+/Tb/pRNGZI0WSElhGDXWgIn3xfcSNQWi079m45c5MlikszzIRsYg==", "dev": true, - "peer": true + "peerDependencies": { + "@bufbuild/protobuf": "^1.4.2", + "@connectrpc/connect": "1.2.0" + } }, "node_modules/@esbuild/android-arm": { "version": "0.19.6", diff --git a/ui/package.json b/ui/package.json index 6242455e..313ac275 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,7 +12,8 @@ "format": "prettier --plugin-search-dir . --write ." }, "devDependencies": { - "@bufbuild/connect-web": "^0.13.0", + "@connectrpc/connect": "^1.1.3", + "@connectrpc/connect-web": "^1.1.3", "@steeze-ui/heroicons": "^2.2.3", "@steeze-ui/svelte-icon": "^1.5.0", "@sveltejs/adapter-auto": "^2.0.0", diff --git a/ui/src/lib/api/client.ts b/ui/src/lib/api/client.ts index 36c32392..38eb43d5 100644 --- a/ui/src/lib/api/client.ts +++ b/ui/src/lib/api/client.ts @@ -1,7 +1,7 @@ import { PortfolioService, SecuritiesService } from '$lib/gen/mgo_connect'; -import { createPromiseClient } from '@bufbuild/connect'; -import { createConnectTransport } from '@bufbuild/connect-web'; -import type { PromiseClient } from '@bufbuild/connect'; +import { createPromiseClient } from '@connectrpc/connect'; +import { createConnectTransport } from '@connectrpc/connect-web'; +import type { PromiseClient } from '@connectrpc/connect'; export function portfolioClient(fetch = window.fetch): PromiseClient { return createPromiseClient( diff --git a/ui/src/lib/components/Button.svelte b/ui/src/lib/components/Button.svelte new file mode 100644 index 00000000..fa414a97 --- /dev/null +++ b/ui/src/lib/components/Button.svelte @@ -0,0 +1,23 @@ + + + diff --git a/ui/src/lib/components/CurrencyInput.svelte b/ui/src/lib/components/CurrencyInput.svelte new file mode 100644 index 00000000..21b41f13 --- /dev/null +++ b/ui/src/lib/components/CurrencyInput.svelte @@ -0,0 +1,30 @@ + + +
+ +
+
+ {symbol} +
+ +
+ {currency} +
+
+
diff --git a/ui/src/lib/components/SecurityComboBox.svelte b/ui/src/lib/components/SecurityComboBox.svelte new file mode 100644 index 00000000..176b6e97 --- /dev/null +++ b/ui/src/lib/components/SecurityComboBox.svelte @@ -0,0 +1,75 @@ + + +
+ + + +
    + {#each securities as value (value.name)} + {@const active = $listbox.active === value} + {@const selected = $listbox.selected === value} +
  • +
    + + {value.displayName} + +
    + + {#if selected} + + + {/if} +
  • + {/each} +
+
+
diff --git a/ui/src/routes/+layout.ts b/ui/src/routes/+layout.ts index 10cf3931..b2bb8e5a 100644 --- a/ui/src/routes/+layout.ts +++ b/ui/src/routes/+layout.ts @@ -1,4 +1,14 @@ import 'inter-ui/inter.css'; import '../app.css'; +import { securitiesClient } from '$lib/api/client'; +import type { LayoutLoad } from './$types'; export const ssr = false; + +export const load = (async ({ fetch }) => { + const client = securitiesClient(fetch); + + const securities = (await client.listSecurities({})).securities; + + return { securities }; +}) satisfies LayoutLoad; diff --git a/ui/src/routes/portfolios/[...name]/+page.ts b/ui/src/routes/portfolios/[...portfolioName]/+layout.ts similarity index 57% rename from ui/src/routes/portfolios/[...name]/+page.ts rename to ui/src/routes/portfolios/[...portfolioName]/+layout.ts index ca359f0c..66666acb 100644 --- a/ui/src/routes/portfolios/[...name]/+page.ts +++ b/ui/src/routes/portfolios/[...portfolioName]/+layout.ts @@ -1,18 +1,17 @@ -import type { PageLoad } from './$types'; +import type { LayoutData } from './$types'; import { error } from '@sveltejs/kit'; -import type { Portfolio } from '$lib/gen/mgo_pb'; import { portfolioClient } from '$lib/api/client'; export const load = (async ({ fetch, params }) => { - if (params.name == undefined) { + if (params.portfolioName == undefined) { throw error(405, 'Required parameter missing'); } const client = portfolioClient(fetch); - console.log(params.name); + console.log(params.portfolioName); - const portfolio = client.getPortfolio({ name: params.name }); - const snapshot = client.getPortfolioSnapshot({ portfolioName: params.name }); + const portfolio = client.getPortfolio({ name: params.portfolioName }); + const snapshot = client.getPortfolioSnapshot({ portfolioName: params.portfolioName }); return { portfolio, snapshot }; -}) as PageLoad; +}) as LayoutData; diff --git a/ui/src/routes/portfolios/[...name]/+page.svelte b/ui/src/routes/portfolios/[...portfolioName]/+page.svelte similarity index 82% rename from ui/src/routes/portfolios/[...name]/+page.svelte rename to ui/src/routes/portfolios/[...portfolioName]/+page.svelte index 60aec4d3..0345fda2 100644 --- a/ui/src/routes/portfolios/[...name]/+page.svelte +++ b/ui/src/routes/portfolios/[...portfolioName]/+page.svelte @@ -8,3 +8,5 @@ + +Add transaction diff --git a/ui/src/routes/portfolios/[...portfolioName]/transactions/[txName]/+page.svelte b/ui/src/routes/portfolios/[...portfolioName]/transactions/[txName]/+page.svelte new file mode 100644 index 00000000..8d94526f --- /dev/null +++ b/ui/src/routes/portfolios/[...portfolioName]/transactions/[txName]/+page.svelte @@ -0,0 +1,100 @@ + + +
+
+
+

Add Transaction

+

+ This allows you to add a transaction to a portfolio. +

+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ + Price + Fees + Taxes + +
+
Total
+
+
+ {currency(total, 'EUR')} +
+
+
+
+
+
+ + +
+ +{error} diff --git a/ui/src/routes/portfolios/[...portfolioName]/transactions/[txName]/+page.ts b/ui/src/routes/portfolios/[...portfolioName]/transactions/[txName]/+page.ts new file mode 100644 index 00000000..1d309b98 --- /dev/null +++ b/ui/src/routes/portfolios/[...portfolioName]/transactions/[txName]/+page.ts @@ -0,0 +1,27 @@ +import { PortfolioEvent, PortfolioEventType } from '$lib/gen/mgo_pb'; +import type { PageLoad } from './$types'; + +export const load = (async ({ params, parent }) => { + const data = await parent(); + const transactionName = params.txName; + const add = transactionName == 'add'; + + let transaction: PortfolioEvent; + if (add) { + // Create a new default import template + transaction = new PortfolioEvent({ + amount: 1, + type: PortfolioEventType.BUY, + portfolioName: data.portfolio.name + }); + } else { + //transaction = await data.client.getImportTemplate({ id: templateId }); + transactionName; + transaction = new PortfolioEvent(); + } + + return { + transaction, + add + }; +}) satisfies PageLoad; diff --git a/ui/src/routes/securities/+page.ts b/ui/src/routes/securities/+page.ts deleted file mode 100644 index 0468d5ea..00000000 --- a/ui/src/routes/securities/+page.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { PageLoad } from './$types'; -import { securitiesClient } from '$lib/api/client'; - -export const load = (async ({ fetch }) => { - const client = securitiesClient(fetch); - - const securities = (await client.listSecurities({})).securities; - - return { securities }; -}) satisfies PageLoad;