diff --git a/backend/app.ts b/backend/app.ts index 576a3b7..733cf0f 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -4,8 +4,6 @@ import path from 'path'; const PUBLIC_PATH = path.join(__dirname, "..", "public"); async function build(opts = {}) { - const nodeEnv = (process.env.NODE_ENV || "").toLowerCase(); - const app = await fastify(opts); app.register(require("fastify-log")); app.register(require("fastify-routes")); diff --git a/backend/dal/index.ts b/backend/dal/index.ts index 6e49460..66cc23b 100644 --- a/backend/dal/index.ts +++ b/backend/dal/index.ts @@ -1,4 +1,12 @@ -const path = require("path"); -const fullFile = process.env.LEDGER_FILE; +import path from 'path'; +import { formatTransaction, Transaction } from 'pta-js'; + +import fs from 'fs/promises'; + +export const fullFile = path.resolve(process.env.K_LEDGER_FILE || ""); export const filename = path.basename(fullFile); + +export function addTransaction(data: Transaction): Promise { + return fs.appendFile(fullFile, `\n${formatTransaction(data)}`, "utf8"); +} diff --git a/backend/journal/index.ts b/backend/journal/index.ts index 0230b62..d883614 100644 --- a/backend/journal/index.ts +++ b/backend/journal/index.ts @@ -1,4 +1,6 @@ -const RESOURCE_ID = "dives"; +import { Transaction } from 'pta-js'; + +import { addTransaction } from '../dal/'; export default function (fastify, opts, done) { const routes = [ @@ -13,12 +15,12 @@ export default function (fastify, opts, done) { method: "POST", path: `/api/journal`, handler: async function (request, reply) { - /* const { guid, ...item } = request.body; - if (!guid) { - return reply.status(405).send({ error: "Missing item id" }); + const trx = request.body as Transaction; + if (!trx.date) { + return reply.status(405).send({ error: "Missing date" }); } - */ + await addTransaction(trx); return reply.status(200).send(); }, }, diff --git a/backend/package.json b/backend/package.json index 37a3bc3..58832c6 100755 --- a/backend/package.json +++ b/backend/package.json @@ -8,6 +8,7 @@ "dependencies": { "@types/node": "^16.11.6", "colors": "^1.1.2", + "dayjs": "^1.10.7", "dotenv": "^10.0.0", "fastify": "^3.15.0", "fastify-log": "^1.2.1", @@ -15,6 +16,7 @@ "fastify-static": "^4.0.1", "luxon": "^2.0.2", "pino-pretty": "^7.2.0", + "pta-js": "file:./../../pta-js", "ts-node": "^10.4.0", "tslib": "^2.2.0", "typescript": "^4.2.4" diff --git a/backend/server.ts b/backend/server.ts index 65eeb00..d034671 100755 --- a/backend/server.ts +++ b/backend/server.ts @@ -1,8 +1,9 @@ -require("dotenv"); +require("dotenv").config(); import colors from 'colors/safe'; import app from './app'; +import { fullFile } from './dal'; import { Server } from './types'; const PORT = 4445; @@ -50,6 +51,7 @@ const init = async () => { //@ts-ignore This is added by a fastify plugin logRoutes(server.routes); + console.log("Journal file:", fullFile); }; init(); diff --git a/backend/yarn.lock b/backend/yarn.lock index 1acc0a4..a311cb6 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -53,11 +53,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@sinclair/typebox@^0.16.5": - version "0.16.7" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.16.7.tgz#3187369b2ff237f57477ef155be4c992e592fa7c" - integrity sha512-d12AkLZJXD30hXBhJSgf33RqGO0NMHIDzsQPYfp6WGoaSuMnSGIUanII2OUbeZFnD/j3Nbl2zifgO2+5tPClCQ== - "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -517,6 +512,11 @@ dateformat@^4.6.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== +dayjs@^1.10.7: + version "1.10.7" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" + integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -1405,6 +1405,9 @@ pstree.remy@^1.1.7: resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== +"pta-js@file:../../pta-js": + version "0.1.0" + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" diff --git a/frontend/package.json b/frontend/package.json index 309179b..0058d88 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,12 +1,13 @@ { "name": "frontend", - "version": "2.1.2", + "version": "0.0.1", "main": "src/index.tsx", "repository": "https://github.com/kajyr/diario.blue", "author": "Carlo Panzi ", "license": "MIT", "dependencies": { "@mantine/core": "^3.0.5", + "@mantine/dates": "^3.1.4", "@mantine/hooks": "^3.0.5", "@types/node": "^16.11.6", "@types/react": "^17.0.3", @@ -15,8 +16,9 @@ "@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/parser": "^5.3.0", "buffer": "^6.0.3", + "dayjs": "^1.10.7", "html-webpack-plugin": "^5.3.1", - "luxon": "^2.0.2", + "pta-js": "file:./../../pta-js", "react": "^17.0.1", "react-dom": "^17.0.1", "react-query": "^3.31.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 18f84cf..0c3f100 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,5 @@ import React, { FC } from 'react'; import { useQuery } from 'react-query'; -import { Route, Routes } from 'react-router-dom'; import Loader from 'atoms/Loader'; @@ -21,9 +20,7 @@ const App: FC = () => { return ( <> - - } /> - + ); diff --git a/frontend/src/helpers/api.ts b/frontend/src/helpers/api.ts new file mode 100644 index 0000000..30f65c2 --- /dev/null +++ b/frontend/src/helpers/api.ts @@ -0,0 +1,26 @@ +export async function callApi(url: string, options: RequestInit = {}) { + const fetchOptions: RequestInit = { + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + ...options, + }; + + const response = await fetch(url, fetchOptions); + // IE11 polyfill for response.ok + if (!("ok" in response)) { + //@ts-ignore + response.ok = response.status >= 200 && response.status < 300; + } + return response; +} + +export function callJsonApi(url, options): Promise { + return callApi(url, options).then((response) => { + if (response.ok) { + return response.json() as Promise; + } + }); +} diff --git a/frontend/src/helpers/clear-transaction.ts b/frontend/src/helpers/clear-transaction.ts new file mode 100644 index 0000000..e7de667 --- /dev/null +++ b/frontend/src/helpers/clear-transaction.ts @@ -0,0 +1,12 @@ +import dayjs from 'dayjs'; +import { Transaction } from 'pta-js'; + +// Removes empty entries +function clearTransaction(trx: Transaction): Transaction { + return { + ...trx, + entries: trx.entries.filter((e) => !(e.account == "" && e.amount === "")), + }; +} + +export default clearTransaction; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index c31710d..5cb76e8 100755 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -23,8 +23,8 @@ render( - } /> - } /> + } /> + } /> diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx deleted file mode 100644 index 60a0a0d..0000000 --- a/frontend/src/pages/Dashboard.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React, { FC } from 'react'; - -const Dashboard: FC = () => { - return
Dashboard
; -}; - -export default Dashboard; diff --git a/frontend/src/pages/dashboard/confirmation-modal.tsx b/frontend/src/pages/dashboard/confirmation-modal.tsx new file mode 100644 index 0000000..9863e37 --- /dev/null +++ b/frontend/src/pages/dashboard/confirmation-modal.tsx @@ -0,0 +1,29 @@ +import React, { FC } from 'react'; + +import clearTransaction from 'helpers/clear-transaction'; + +import { Button, Code, Group, Modal } from '@mantine/core'; + +import { formatTransaction, Transaction } from 'pta-js'; + +const ConfirmationModal: FC<{ + data: Transaction; + onCancel: () => void; + onConfirm: (data: Transaction) => void; +}> = ({ data, onCancel, onConfirm }) => { + const clean = clearTransaction(data); + const trxstr = formatTransaction(clean); + return ( + + {trxstr} + + + + + + ); +}; + +export default ConfirmationModal; diff --git a/frontend/src/pages/dashboard/index.tsx b/frontend/src/pages/dashboard/index.tsx new file mode 100644 index 0000000..8f9ca94 --- /dev/null +++ b/frontend/src/pages/dashboard/index.tsx @@ -0,0 +1,144 @@ +import React, { FC, useState } from 'react'; + +import { callApi } from 'helpers/api'; +import clearTransaction from 'helpers/clear-transaction'; + +import { Button, Group, LoadingOverlay, Paper, TextInput, Title } from '@mantine/core'; +import { DatePicker } from '@mantine/dates'; +import { useForm } from '@mantine/hooks'; + +import { Transaction } from 'pta-js'; + +import ConfirmationModal from './confirmation-modal'; + +const EMPTY_ENTRY = { account: "", amount: "" }; + +const Dashboard: FC = () => { + const { onSubmit, values, setFieldValue, setValues, reset } = useForm({ + initialValues: { + date: new Date(), + description: "", + entries: [EMPTY_ENTRY], + }, + }); + + const [modalOpen, setModalOpen] = useState(false); + const [showOverlay, setOverlay] = useState(false); + + const handleSubmit = () => { + setModalOpen(true); + }; + + function addRow() { + setValues((state) => ({ + ...state, + entries: state.entries.concat({ ...EMPTY_ENTRY }), + })); + } + + function updateRow(idx: number, field: string, value: string) { + setValues((state) => ({ + ...state, + entries: state.entries.map((e, i) => { + if (i !== idx) { + return e; + } + + return { ...e, [field]: value }; + }), + })); + } + + function removeRow(idx: number) { + return () => { + setValues((state) => ({ + ...state, + entries: state.entries.filter((e, i) => i !== idx), + })); + }; + } + + async function handleModalConfirm(data: Transaction) { + setModalOpen(false); + setOverlay(true); + const response = await callApi("/api/journal", { + method: "POST", + body: JSON.stringify(data), + }); + setOverlay(false); + if (response.ok) { + reset(); + } + } + + return ( +
+ +
+ + Add + date && setFieldValue("date", date)} + /> + + setFieldValue("description", event.currentTarget.value) + } + /> + {values.entries.map((entry, i) => ( + + + updateRow(i, "account", event.currentTarget.value) + } + /> + + updateRow(i, "amount", event.currentTarget.value) + } + /> + + {i !== 0 && ( + + )} + + ))} + + + + +
+ {modalOpen && ( + setModalOpen(false)} + onConfirm={handleModalConfirm} + /> + )} +
+ ); +}; + +export default Dashboard; diff --git a/frontend/src/templates/Page.tsx b/frontend/src/templates/Page.tsx index 0ce8470..acd5272 100755 --- a/frontend/src/templates/Page.tsx +++ b/frontend/src/templates/Page.tsx @@ -1,6 +1,8 @@ import React, { FC } from 'react'; import styled from 'styled-components'; +import { Title } from '@mantine/core'; + const Wrapper = styled.div` display: flex; flex-direction: column; @@ -8,10 +10,6 @@ const Wrapper = styled.div` header { margin-bottom: 15px; - - h1 { - margin-bottom: 5px; - } } `; @@ -22,7 +20,7 @@ const Body = styled.main` const Page: FC<{ filename: string }> = ({ filename, children }) => (
-

kLedger UI

+ kLedger UI
File in use: {filename}
{children} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 2f51de1..6a15de0 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -628,6 +628,15 @@ react-popper "^2.2.5" react-textarea-autosize "^8.3.2" +"@mantine/dates@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@mantine/dates/-/dates-3.1.4.tgz#9cbabcbb3c2965307780249bbe13a2d54e2e56e2" + integrity sha512-FHHB+Pl2N6wScYexxfD6vZ0OQIUmF0d3NBtCPn/wCbGvFQlcRBkBqSsgoyXSwLtfXut9J1vPoatzYDmVTpUF4A== + dependencies: + "@popperjs/core" "^2.9.3" + clsx "^1.1.1" + react-popper "^2.2.5" + "@mantine/hooks@^3.0.5": version "3.1.4" resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-3.1.4.tgz#fc4f95bbb7b5a46ca61a060b9eeb1adde73f908a" @@ -1661,6 +1670,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +dayjs@^1.10.7: + version "1.10.7" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" + integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== + debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" @@ -3340,11 +3354,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -luxon@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.0.2.tgz#11f2cd4a11655fdf92e076b5782d7ede5bcdd133" - integrity sha512-ZRioYLCgRHrtTORaZX1mx+jtxKtKuI5ZDvHNAmqpUzGqSrR+tL4FVLn/CUGMA3h0+AKD1MAxGI5GnCqR5txNqg== - make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -3769,6 +3778,9 @@ psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== +"pta-js@file:./../../pta-js": + version "0.1.0" + punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"