From e5bbd266e9a954f1cd3cd853865f0c4e37c1eb78 Mon Sep 17 00:00:00 2001 From: Linus Miller Date: Thu, 4 Dec 2025 20:54:23 +0100 Subject: [PATCH] add transactions and random fixes --- .bruno/BRF/api-transactions.bru | 15 +++ client/public/components/accounts_page.tsx | 2 +- client/public/components/app.tsx | 31 +++++- client/public/components/entries_page.tsx | 11 ++- client/public/components/entry_page.tsx | 16 +--- client/public/components/header.module.scss | 16 ++++ client/public/components/header.tsx | 3 +- client/public/components/object.tsx | 15 ++- .../components/results_page.module.scss | 19 ++-- client/public/components/results_page.tsx | 11 ++- .../public/components/transactions_page.tsx | 96 +++++++++++++++++++ client/public/routes.ts | 7 ++ client/public/styles/main.scss | 16 ++++ client/shared/utils/throttle.ts | 24 +++++ server/routes/api.ts | 2 + server/routes/api/financial_years.ts | 2 +- server/routes/api/objects.ts | 2 +- server/routes/api/transactions.ts | 61 ++++++++++++ 18 files changed, 311 insertions(+), 38 deletions(-) create mode 100644 .bruno/BRF/api-transactions.bru create mode 100644 client/public/components/header.module.scss create mode 100644 client/public/components/transactions_page.tsx create mode 100644 client/shared/utils/throttle.ts create mode 100644 server/routes/api/transactions.ts diff --git a/.bruno/BRF/api-transactions.bru b/.bruno/BRF/api-transactions.bru new file mode 100644 index 0000000..fccb7c1 --- /dev/null +++ b/.bruno/BRF/api-transactions.bru @@ -0,0 +1,15 @@ +meta { + name: /api/transactions + type: http + seq: 14 +} + +get { + url: {{base_url}}/api/transactions + body: none + auth: inherit +} + +settings { + encodeUrl: true +} diff --git a/client/public/components/accounts_page.tsx b/client/public/components/accounts_page.tsx index 318df3f..4709118 100644 --- a/client/public/components/accounts_page.tsx +++ b/client/public/components/accounts_page.tsx @@ -20,7 +20,7 @@ const AccountsPage = () => {

Accounts

- +
diff --git a/client/public/components/app.tsx b/client/public/components/app.tsx index 76f6858..3b0b139 100644 --- a/client/public/components/app.tsx +++ b/client/public/components/app.tsx @@ -1,14 +1,43 @@ import { h } from 'preact' +import { useCallback, useEffect } from 'preact/hooks' import { LocationProvider, Route, Router } from 'preact-iso' +import { get } from 'lowline' import Head from './head.ts' import Footer from './footer.tsx' import Header from './header.tsx' import ErrorPage from './error_page.tsx' import routes from '../routes.ts' +import throttle from '../../shared/utils/throttle.ts' import s from './app.module.scss' +const remember = throttle(function remember() { + window.history.replaceState( + { + ...window.history.state, + scrollTop: window.scrollY, + }, + '', + null, + ) +}, 100) + export default function App({ error, url, title }) { + useEffect(() => { + addEventListener('scroll', remember) + + return () => removeEventListener('scroll', remember) + }) + + const onRouteChange = useCallback(() => { + const offset = get(window.history, 'state.scrollTop') + console.log('offset', offset) + + setTimeout(() => { + window.scrollTo(0, offset || 0) + }, 100) + }, []) + return (
@@ -22,7 +51,7 @@ export default function App({ error, url, title }) { {error ? ( ) : ( - + {routes.map((route) => ( ))} diff --git a/client/public/components/entries_page.tsx b/client/public/components/entries_page.tsx index f980a1e..f0e0698 100644 --- a/client/public/components/entries_page.tsx +++ b/client/public/components/entries_page.tsx @@ -34,7 +34,7 @@ const EntriesPage = () => { setJournals(journals) }) rek('/api/financial-years').then((financialYears) => { - setFinancialYears(financialYears.toReversed()) + setFinancialYears(financialYears) }) }, []) @@ -68,7 +68,7 @@ const EntriesPage = () => {

{selectedJournal} : {selectedYear}

-
Number Description
+
@@ -85,10 +85,13 @@ const EntriesPage = () => { - + - + ))} diff --git a/client/public/components/entry_page.tsx b/client/public/components/entry_page.tsx index b41e83c..4283a87 100644 --- a/client/public/components/entry_page.tsx +++ b/client/public/components/entry_page.tsx @@ -11,10 +11,6 @@ const EntriesPage = () => { const location = useLocation() const route = useRoute() - // console.dir(route) - // console.dir(location) - console.log(entry) - useEffect(() => { rek(`/api/entries/${route.params.id}`).then((entry) => { setEntry(entry) @@ -23,8 +19,6 @@ const EntriesPage = () => { if (!entry) return - console.log(entry) - return (
@@ -37,7 +31,7 @@ const EntriesPage = () => { Entry {entry.journal} {entry.number} -
ID {entry.id} {entry.number} + {entry.journal} + {entry.number} + {entry.entryDate?.slice(0, 10)} {entry.transactionDate?.slice(0, 10)}{entry.amount}{entry.amount} {entry.description}
+
@@ -58,14 +52,14 @@ const EntriesPage = () => { - +
ID{entry.number} {entry.entryDate?.slice(0, 10)} {entry.transactionDate?.slice(0, 10)}{entry.amount}{entry.amount} {entry.description}

Transactions

- +
@@ -78,8 +72,8 @@ const EntriesPage = () => { {entry?.transactions?.map((transaction) => ( - - + + ))} diff --git a/client/public/components/header.module.scss b/client/public/components/header.module.scss new file mode 100644 index 0000000..b0d10a4 --- /dev/null +++ b/client/public/components/header.module.scss @@ -0,0 +1,16 @@ +@use '../../shared/styles/utils'; + +.nav { + > ul { + @include utils.wipe-list(); + + display: flex; + + > li { + > a { + display: block; + padding: 5px; + } + } + } +} diff --git a/client/public/components/header.tsx b/client/public/components/header.tsx index cf71110..9a02ce8 100644 --- a/client/public/components/header.tsx +++ b/client/public/components/header.tsx @@ -1,9 +1,10 @@ import { h } from 'preact' +import s from './header.module.scss' const Header = ({ routes }) => (

BRF Tegeltrasten

-
Account
{transaction.account_number}{transaction.amount >= 0 ? formatNumber(transaction.amount) : null}{transaction.amount < 0 ? formatNumber(Math.abs(transaction.amount)) : null}{transaction.amount >= 0 ? formatNumber(transaction.amount) : null}{transaction.amount < 0 ? formatNumber(Math.abs(transaction.amount)) : null} {transaction.description}
+
+ + + + + + + + {transactions.map((transaction) => ( + - + ))} diff --git a/client/public/components/results_page.module.scss b/client/public/components/results_page.module.scss index 014d7b6..73f7bcc 100644 --- a/client/public/components/results_page.module.scss +++ b/client/public/components/results_page.module.scss @@ -1,16 +1,9 @@ .table { - td:nth-child(3), - td:nth-child(3) ~ td { - text-align: right; - } - - tr:hover { - background: #eee; - } - - td, - th { - border: 1px solid #ccc; - padding: 3px 5px; + a { + text-decoration: none; + color: black; + &:hover { + font-weight: bold; + } } } diff --git a/client/public/components/results_page.tsx b/client/public/components/results_page.tsx index 5e61506..29a63cc 100644 --- a/client/public/components/results_page.tsx +++ b/client/public/components/results_page.tsx @@ -1,5 +1,6 @@ import { h } from 'preact' import { useEffect, useState } from 'preact/hooks' +import cn from 'classnames' import rek from 'rek' import Head from './head.ts' import Result from './result.tsx' @@ -13,7 +14,7 @@ const ResultsPage = () => { useEffect(() => { rek(`/api/results`).then(setResults) - rek(`/api/financial-years`).then((years) => setYears(years.map((fy) => fy.year).toReversed())) + rek(`/api/financial-years`).then((years) => setYears(years.map((fy) => fy.year))) }, []) return ( @@ -24,7 +25,7 @@ const ResultsPage = () => {

Results

{years.length && results.length && ( -
EntryDateAccountAmount
+ {transaction.entryId} + {transaction.transactionDate.slice(0, 10)} {transaction.accountNumber}{transaction.amount}{transaction.amount}
+
@@ -40,7 +41,11 @@ const ResultsPage = () => { {years.map((year) => ( - + ))} ))} diff --git a/client/public/components/transactions_page.tsx b/client/public/components/transactions_page.tsx new file mode 100644 index 0000000..1a9141f --- /dev/null +++ b/client/public/components/transactions_page.tsx @@ -0,0 +1,96 @@ +import { h } from 'preact' +import { useCallback, useEffect, useState } from 'preact/hooks' +import { useLocation } from 'preact-iso' +import { isEmpty } from 'lowline' +import qs from 'mini-qs' +import rek from 'rek' +import Head from './head.ts' +import serializeForm from '../../shared/utils/serialize_form.ts' +import { formatNumber } from '../utils/format_number.ts' + +const TransactionsPage = () => { + const [financialYears, setFinancialYears] = useState([]) + const [transactions, setTransactions] = useState([]) + + const location = useLocation() + + console.log(location) + + const onSubmit = useCallback((e: SubmitEvent) => { + e.preventDefault() + + const values = serializeForm(e.target as HTMLFormElement) + + const search = !isEmpty(values) ? '?' + qs.stringify(values) : '' + + location.route(`/transactions${search}`) + }, []) + + useEffect(() => { + rek('/api/financial-years').then((financialYears) => { + setFinancialYears(financialYears) + }) + }, []) + + useEffect(() => { + const search = location.url.split('?')[1] || '' + + rek(`/api/transactions${search ? '?' + search : ''}`).then((transactions) => { + setTransactions(transactions) + }) + }, [location.url]) + + return ( +
+ + : Transactions + + +
+ + + + + +

Transactions

+
+
Account{result.accountNumber} {result.description}{formatNumber(result[year])} + + {formatNumber(result[year])} + +
+ + + + + + + + + + + + {transactions.map((transaction) => ( + + + + + + + + + + + ))} + +
DateAccountDebitCreditDescriptionEntryEntry DescriptionInvoice
{transaction.transactionDate.slice(0, 10)}{transaction.accountNumber}{transaction.amount >= 0 ? formatNumber(transaction.amount) : null}{transaction.amount < 0 ? formatNumber(Math.abs(transaction.amount)) : null}{transaction.description} + {transaction.entryId} + {transaction.entryDescription} + {transaction.invoiceId} +
+ + + ) +} + +export default TransactionsPage diff --git a/client/public/routes.ts b/client/public/routes.ts index e96d0ef..b5a2fce 100644 --- a/client/public/routes.ts +++ b/client/public/routes.ts @@ -7,6 +7,7 @@ import InvoicesBySupplier from './components/invoices_by_supplier_page.tsx' import Objects from './components/objects_page.tsx' import Results from './components/results_page.tsx' import Start from './components/start_page.tsx' +import Transactions from './components/transactions_page.tsx' export default [ { @@ -66,4 +67,10 @@ export default [ title: 'Results', component: Results, }, + { + path: '/transactions', + name: 'transactions', + title: 'Transactions', + component: Transactions, + }, ] diff --git a/client/public/styles/main.scss b/client/public/styles/main.scss index 628963c..4fbe872 100644 --- a/client/public/styles/main.scss +++ b/client/public/styles/main.scss @@ -7,4 +7,20 @@ table { border-collapse: collapse; border-spacing: 0; + + &.grid { + tr:hover { + background: #eee; + } + + td, + th { + border: 1px solid #ccc; + padding: 3px 5px; + } + } +} + +.tar { + text-align: right; } diff --git a/client/shared/utils/throttle.ts b/client/shared/utils/throttle.ts new file mode 100644 index 0000000..0ee7f26 --- /dev/null +++ b/client/shared/utils/throttle.ts @@ -0,0 +1,24 @@ +// copied and modified from https://github.com/sindresorhus/throttleit/blob/ed1d22c70a964ef0299d0400dbfd1fbedef56a59/index.js + +export default function throttle(fnc, wait) { + let timeout + let lastCallTime = 0 + + return function throttled(...args) { + clearTimeout(timeout) + + const now = Date.now() + const timeSinceLastCall = now - lastCallTime + const delayForNextCall = wait - timeSinceLastCall + + if (delayForNextCall <= 0) { + lastCallTime = now + fnc(...args) + } else { + timeout = setTimeout(() => { + lastCallTime = Date.now() + fnc(...args) + }, delayForNextCall) + } + } +} diff --git a/server/routes/api.ts b/server/routes/api.ts index b4579e5..810d040 100644 --- a/server/routes/api.ts +++ b/server/routes/api.ts @@ -10,6 +10,7 @@ import journals from './api/journals.ts' import objects from './api/objects.ts' import results from './api/results.ts' import suppliers from './api/suppliers.ts' +import transactions from './api/transactions.ts' export const FinancialYear = Type.Object({ year: Type.Number(), @@ -28,6 +29,7 @@ const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { fastify.register(objects, { prefix: '/objects' }) fastify.register(results, { prefix: '/results' }) fastify.register(suppliers, { prefix: '/suppliers' }) + fastify.register(transactions, { prefix: '/transactions' }) done() } diff --git a/server/routes/api/financial_years.ts b/server/routes/api/financial_years.ts index ee30a0a..2540767 100644 --- a/server/routes/api/financial_years.ts +++ b/server/routes/api/financial_years.ts @@ -7,7 +7,7 @@ const financialYearRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => url: '/', method: 'GET', handler() { - return knex('financialYear').select('*') + return knex('financialYear').select('*').orderBy('startDate', 'desc') }, }) diff --git a/server/routes/api/objects.ts b/server/routes/api/objects.ts index ba10bc8..cc67dd3 100644 --- a/server/routes/api/objects.ts +++ b/server/routes/api/objects.ts @@ -25,7 +25,7 @@ const objectRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { }, async handler(req) { return knex('transaction AS t') - .select('e.transactionDate', 't.accountNumber', 't.amount') + .select('t.entryId', 'e.transactionDate', 't.accountNumber', 't.amount') .innerJoin('transactions_to_objects AS to', function () { this.on('t.id', 'to.transactionId') }) diff --git a/server/routes/api/transactions.ts b/server/routes/api/transactions.ts new file mode 100644 index 0000000..929fd9b --- /dev/null +++ b/server/routes/api/transactions.ts @@ -0,0 +1,61 @@ +import _ from 'lodash' +import { Type, type Static, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' +import knex from '../../lib/knex.ts' +import StatusError from '../../lib/status_error.ts' + +const transactionRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { + fastify.route({ + url: '/', + method: 'GET', + schema: { + querystring: Type.Object({ + year: Type.Optional(Type.Number()), + accountNumber: Type.Optional(Type.Number()), + }), + }, + async handler(req) { + const query: { financialYearId?: number; accountNumber?: number } = {} + + if (req.query.year) { + const year = await knex('financialYear').first('*').where('year', req.query.year) + + if (!year) throw new StatusError(404, `Year ${req.query.year} not found.`) + query.financialYearId = year.id + } + + if (req.query.accountNumber) { + query.accountNumber = req.query.accountNumber + } + + return knex('transaction AS t') + .select( + 't.accountNumber', + 'e.transactionDate', + 't.entryId', + 't.amount', + 't.description', + 't.invoiceId', + 'e.description AS entryDescription', + ) + .innerJoin('entry AS e', 't.entry_id', 'e.id') + .where(query) + }, + }) + + fastify.route({ + url: '/:id', + method: 'GET', + schema: { + params: Type.Object({ + id: Type.Number(), + }), + }, + async handler(req) { + return knex('transaction').first('*').where('id', req.params.id) + }, + }) + + done() +} + +export default transactionRoutes