From 60bdd909a7f6ece1f1c93edf9cfaadb8d84d918f Mon Sep 17 00:00:00 2001 From: Linus Miller Date: Mon, 24 Nov 2025 17:10:03 +0100 Subject: [PATCH] create results page and routes --- .bruno/BRF/Financial Years.bru | 16 ++++ .bruno/BRF/Result.bru | 16 ++++ .bruno/BRF/Results.bru | 16 ++++ .bruno/BRF/bruno.json | 15 +++ .bruno/BRF/collection.bru | 3 + client/public/components/other_page.tsx | 15 --- client/public/components/result.tsx | 31 +++++++ .../components/results_page.module.scss | 5 + client/public/components/results_page.tsx | 41 +++++++++ client/public/routes.ts | 10 +- docker/postgres/01-schema.sql | 13 ++- package.json | 5 +- pnpm-lock.yaml | 28 ++++++ server/lib/parse_stream.ts | 6 +- server/routes/api.ts | 92 ++++++++++++------- server/server.ts | 4 +- 16 files changed, 257 insertions(+), 59 deletions(-) create mode 100644 .bruno/BRF/Financial Years.bru create mode 100644 .bruno/BRF/Result.bru create mode 100644 .bruno/BRF/Results.bru create mode 100644 .bruno/BRF/bruno.json create mode 100644 .bruno/BRF/collection.bru delete mode 100644 client/public/components/other_page.tsx create mode 100644 client/public/components/result.tsx create mode 100644 client/public/components/results_page.module.scss create mode 100644 client/public/components/results_page.tsx diff --git a/.bruno/BRF/Financial Years.bru b/.bruno/BRF/Financial Years.bru new file mode 100644 index 0000000..78fd030 --- /dev/null +++ b/.bruno/BRF/Financial Years.bru @@ -0,0 +1,16 @@ +meta { + name: Financial Years + type: http + seq: 2 +} + +get { + url: {{base_url}}/api/financial-years + body: none + auth: inherit +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/.bruno/BRF/Result.bru b/.bruno/BRF/Result.bru new file mode 100644 index 0000000..e9f9203 --- /dev/null +++ b/.bruno/BRF/Result.bru @@ -0,0 +1,16 @@ +meta { + name: Result + type: http + seq: 2 +} + +get { + url: {{base_url}}/api/results/2018 + body: none + auth: inherit +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/.bruno/BRF/Results.bru b/.bruno/BRF/Results.bru new file mode 100644 index 0000000..b3d8eb2 --- /dev/null +++ b/.bruno/BRF/Results.bru @@ -0,0 +1,16 @@ +meta { + name: Results + type: http + seq: 4 +} + +get { + url: {{base_url}}/api/results + body: none + auth: inherit +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/.bruno/BRF/bruno.json b/.bruno/BRF/bruno.json new file mode 100644 index 0000000..bf8971a --- /dev/null +++ b/.bruno/BRF/bruno.json @@ -0,0 +1,15 @@ +{ + "version": "1", + "name": "BRF", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ], + "size": 0, + "filesCount": 0, + "presets": { + "requestType": "http", + "requestUrl": "{{base_url}}/" + } +} \ No newline at end of file diff --git a/.bruno/BRF/collection.bru b/.bruno/BRF/collection.bru new file mode 100644 index 0000000..7b0de0c --- /dev/null +++ b/.bruno/BRF/collection.bru @@ -0,0 +1,3 @@ +vars:pre-request { + base_url: https://brf.local +} diff --git a/client/public/components/other_page.tsx b/client/public/components/other_page.tsx deleted file mode 100644 index 2ca1e39..0000000 --- a/client/public/components/other_page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { h } from 'preact' -import Head from './head.ts' - -const OtherPage = () => ( -
- - : Other - - -

Other Page

-

Not the page where it begins

-
-) - -export default OtherPage diff --git a/client/public/components/result.tsx b/client/public/components/result.tsx new file mode 100644 index 0000000..4cad2eb --- /dev/null +++ b/client/public/components/result.tsx @@ -0,0 +1,31 @@ +import { h, type FunctionalComponent } from 'preact' +import { useEffect, useState } from 'preact/hooks' +import rek from 'rek' + +interface Props { + year: number +} + +const Result: FunctionalComponent = ({ year }) => { + const [result, setResults] = useState([]) + + useEffect(() => { + rek(`/api/results/${year}`).then(setResults) + }, [year]) + + return ( + + + {result.map((result) => ( + + + + + + ))} + +
{result.accountNumber}{result.description}{result.amount}
+ ) +} + +export default Result diff --git a/client/public/components/results_page.module.scss b/client/public/components/results_page.module.scss new file mode 100644 index 0000000..f7918e1 --- /dev/null +++ b/client/public/components/results_page.module.scss @@ -0,0 +1,5 @@ +.years { + display: flex; + flex-direction: row-reverse; + justify-content: start; +} diff --git a/client/public/components/results_page.tsx b/client/public/components/results_page.tsx new file mode 100644 index 0000000..088ab2d --- /dev/null +++ b/client/public/components/results_page.tsx @@ -0,0 +1,41 @@ +import { h } from 'preact' +import { useEffect, useState } from 'preact/hooks' +import rek from 'rek' +import Head from './head.ts' +import Result from './result.tsx' +import s from './results_page.module.scss' + +const ResultsPage = () => { + const [financialYears, setFinancialYears] = useState([]) + const [currentYear, setCurrentYear] = useState(null) + + useEffect(() => { + rek('/api/financial-years').then((financialYears) => { + setFinancialYears(financialYears) + setCurrentYear(financialYears[financialYears.length - 1].year) + }) + }, []) + + return ( +
+ + : Results + + +

Results

+
+ {financialYears.map((financialYear) => ( + + ))} +
+ {currentYear ? ( +
+

{currentYear}

+ +
+ ) : null} +
+ ) +} + +export default ResultsPage diff --git a/client/public/routes.ts b/client/public/routes.ts index d2cf495..c47c16d 100644 --- a/client/public/routes.ts +++ b/client/public/routes.ts @@ -1,5 +1,5 @@ import Start from './components/start_page.tsx' -import Other from './components/other_page.tsx' +import Results from './components/results_page.tsx' export default [ { @@ -9,9 +9,9 @@ export default [ component: Start, }, { - path: '/other', - name: 'other', - title: 'Other', - component: Other, + path: '/results', + name: 'results', + title: 'Results', + component: Results, }, ] diff --git a/docker/postgres/01-schema.sql b/docker/postgres/01-schema.sql index ef6c93d..b7d43f7 100644 --- a/docker/postgres/01-schema.sql +++ b/docker/postgres/01-schema.sql @@ -2,7 +2,7 @@ -- PostgreSQL database dump -- -\restrict EQkcX1mt4Oqej8UZL2Z1qyvnzIAf3O4TYefngsqIN91Lr6JLNTawzD4OIRSJr2s +\restrict H2xBPQq6I6IQHZkAgiuKoY9lao4r4KAPtiyNvDE9oc0DN75cJ1gNbkoGFqutWDp -- Dumped from database version 18.1 -- Dumped by pg_dump version 18.1 @@ -162,6 +162,7 @@ ALTER SEQUENCE public.entry_id_seq OWNED BY public.entry.id; CREATE TABLE public.financial_year ( id integer NOT NULL, + year integer NOT NULL, start_date date NOT NULL, end_date date NOT NULL ); @@ -402,6 +403,14 @@ ALTER TABLE ONLY public.financial_year ADD CONSTRAINT financial_year_start_date_end_date_key UNIQUE (start_date, end_date); +-- +-- Name: financial_year financial_year_year_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.financial_year + ADD CONSTRAINT financial_year_year_key UNIQUE (year); + + -- -- Name: journal journal_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -486,5 +495,5 @@ ALTER TABLE ONLY public.transactions_to_objects -- PostgreSQL database dump complete -- -\unrestrict EQkcX1mt4Oqej8UZL2Z1qyvnzIAf3O4TYefngsqIN91Lr6JLNTawzD4OIRSJr2s +\unrestrict H2xBPQq6I6IQHZkAgiuKoY9lao4r4KAPtiyNvDE9oc0DN75cJ1gNbkoGFqutWDp diff --git a/package.json b/package.json index 4ca30cd..7c7c9dc 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@bmp/highlight-stack": "^0.1.2", "@fastify/middie": "^9.0.3", "@fastify/static": "^8.3.0", + "@fastify/type-provider-typebox": "^6.1.0", "chalk": "^5.6.2", "fastify": "^5.6.2", "fastify-plugin": "^5.1.0", @@ -37,7 +38,8 @@ "pg-protocol": "^1.10.3", "pino-abstract-transport": "^3.0.0", "preact": "^10.27.2", - "preact-router": "^4.1.2" + "preact-router": "^4.1.2", + "rek": "^0.8.1" }, "devDependencies": { "@babel/core": "^7.26.10", @@ -53,6 +55,7 @@ "oxlint": "^1.29.0", "prettier": "^3.5.3", "sass": "^1.85.1", + "typebox": "^1.0.55", "typescript": "^5.8.2", "vite": "^7.2.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03b78f9..9f9c7a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@fastify/static': specifier: ^8.3.0 version: 8.3.0 + '@fastify/type-provider-typebox': + specifier: ^6.1.0 + version: 6.1.0(typebox@1.0.55) chalk: specifier: ^5.6.2 version: 5.6.2 @@ -53,6 +56,9 @@ importers: preact-router: specifier: ^4.1.2 version: 4.1.2(preact@10.27.2) + rek: + specifier: ^0.8.1 + version: 0.8.1 devDependencies: '@babel/core': specifier: ^7.26.10 @@ -93,6 +99,9 @@ importers: sass: specifier: ^1.85.1 version: 1.94.2 + typebox: + specifier: ^1.0.55 + version: 1.0.55 typescript: specifier: ^5.8.2 version: 5.9.3 @@ -591,6 +600,11 @@ packages: '@fastify/static@8.3.0': resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==} + '@fastify/type-provider-typebox@6.1.0': + resolution: {integrity: sha512-k29cOitDRcZhMXVjtRq0+caKxdWoArz7su+dQWGzGWnFG+fSKhevgiZ7nexHWuXOEEQzgJlh6cptIMu69beaTA==} + peerDependencies: + typebox: ^1.0.13 + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -1844,6 +1858,9 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + rek@0.8.1: + resolution: {integrity: sha512-DXklCeA33/W9oaKjn/Upiwpdt7ZYqGJE2+1/l5v9iXwRzlfTEDtG7wMJLSWXftXgQz7/IlOjmLgOY/5OwbFPOA==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2076,6 +2093,9 @@ packages: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} + typebox@1.0.55: + resolution: {integrity: sha512-TP02wN0B6tDZngprrGVu/Z9s/QUyVEmR7VIg1yEOtsqyDdXXEoQPSfWdkD2PsA2lGLxu6GgwOTtGZVS9CAoERg==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -2604,6 +2624,10 @@ snapshots: fastq: 1.19.1 glob: 11.1.0 + '@fastify/type-provider-typebox@6.1.0(typebox@1.0.55)': + dependencies: + typebox: 1.0.55 + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -3833,6 +3857,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + rek@0.8.1: {} + require-from-string@2.0.2: {} resolve-from@5.0.0: {} @@ -4080,6 +4106,8 @@ snapshots: dependencies: punycode: 2.3.1 + typebox@1.0.55: {} + typescript@5.9.3: {} undici-types@7.16.0: {} diff --git a/server/lib/parse_stream.ts b/server/lib/parse_stream.ts index 0a78c7f..c14b63c 100644 --- a/server/lib/parse_stream.ts +++ b/server/lib/parse_stream.ts @@ -158,7 +158,11 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod if (yearNumber !== 0) continue - currentYear = (await trx('financial_year').insert({ startDate, endDate }).returning('*'))[0] + currentYear = ( + await trx('financial_year') + .insert({ year: startDate.slice(0, 4), startDate, endDate }) + .returning('*') + )[0] break } diff --git a/server/routes/api.ts b/server/routes/api.ts index 6c7241b..4e8bd6b 100644 --- a/server/routes/api.ts +++ b/server/routes/api.ts @@ -1,59 +1,85 @@ -import { type FastifyPluginCallback } from 'fastify' +import { Type, type Static, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import knex from '../lib/knex.ts' -const apiRoutes: FastifyPluginCallback = (fastify, _, done) => { +export const FinancialYear = Type.Object({ + year: Type.Number(), + startDate: Type.String(), + endDate: Type.String(), +}) + +export type FinancialYearType = Static + +const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { fastify.route({ - url: '/result', + url: '/financial-years', method: 'GET', - async handler(req, res) { + handler() { + return knex('financialYear').select('*') + }, + }) + + fastify.route({ + url: '/results', + method: 'GET', + async handler() { const years = await knex('financialYear').select('*') + const accounts = await knex('account').select('*') + return Promise.all( years.map((year) => - knex('account') - .select('account.number', 'account.description') - .sum('transaction.amount as amount') - .innerJoin('transaction', function () { - this.on('transaction.accountNumber', '=', 'account.number') + knex('account AS a') + .select('a.number', 'a.description') + .sum('t.amount as amount') + .innerJoin('transaction AS t', function () { + this.on('t.accountNumber', '=', 'a.number') }) - .innerJoin('entry', function () { - this.on('transaction.entryId', '=', 'entry.id') + .innerJoin('entry AS e', function () { + this.on('t.entryId', '=', 'e.id') }) - .groupBy('account.number', 'account.description') - .where('account.number', '>=', 3000) - .where('entry.financialYearId', year.id) - .orderBy('account.number') + .groupBy('a.number', 'a.description') + .where('a.number', '>=', 3000) + .where('e.financialYearId', year.id) + .orderBy('a.number') .then((result) => ({ startDate: year.startDate, endDate: year.endDate, result, })), ), - ) + ).then((years) => ({ + accounts, + years, + })) }, }) fastify.route({ - url: '/result/:year', + url: '/results/:year', method: 'GET', - async handler(req, res) { - const year = await knex('financialYear').first('*').where('startDate', `${req.params.year}0101`) + schema: { + params: Type.Object({ + year: Type.Number(), + }), + }, + async handler(req) { + const year = await knex('financialYear').first('*').where('year', req.params.year) - const result = await knex('account') - .select('account.number', 'account.description') - .sum('transaction.amount as amount') - .innerJoin('transaction', function () { - this.on('transaction.accountNumber', '=', 'account.number') - }) - .innerJoin('entry', function () { - this.on('transaction.entryId', '=', 'entry.id') - }) - .groupBy('account.number', 'account.description') - .where('account.number', '>=', 3000) - .where('entry.financialYearId', year.id) - .orderBy('account.number') + if (!year) return null - return result + return knex('transaction AS t') + .select('t.accountNumber', 'a.description') + .sum('t.amount as amount') + .innerJoin('account AS a', function () { + this.on('t.accountNumber', '=', 'a.number') + }) + .innerJoin('entry AS e', function () { + this.on('t.entryId', '=', 'e.id') + }) + .groupBy('t.accountNumber', 'a.description') + .where('t.accountNumber', '>=', 3000) + .where('e.financialYearId', year.id) + .orderBy('t.accountNumber') }, }) diff --git a/server/server.ts b/server/server.ts index 86d12c3..eb8a418 100644 --- a/server/server.ts +++ b/server/server.ts @@ -1,4 +1,4 @@ -import fastify from 'fastify' +import fastify, { type FastifyServerOptions } from 'fastify' import StatusError from './lib/status_error.ts' import env from './env.ts' import ErrorHandler from './handlers/error.ts' @@ -6,7 +6,7 @@ import vitePlugin from './plugins/vite.ts' import apiRoutes from './routes/api.ts' import templatePublic from './templates/public.ts' -export default async (options) => { +export default async (options: FastifyServerOptions) => { const server = fastify(options) server.setNotFoundHandler(() => {