create results page and routes

This commit is contained in:
Linus Miller 2025-11-24 17:10:03 +01:00
parent e7f70b9295
commit 60bdd909a7
16 changed files with 257 additions and 59 deletions

View File

@ -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
}

16
.bruno/BRF/Result.bru Normal file
View File

@ -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
}

16
.bruno/BRF/Results.bru Normal file
View File

@ -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
}

15
.bruno/BRF/bruno.json Normal file
View File

@ -0,0 +1,15 @@
{
"version": "1",
"name": "BRF",
"type": "collection",
"ignore": [
"node_modules",
".git"
],
"size": 0,
"filesCount": 0,
"presets": {
"requestType": "http",
"requestUrl": "{{base_url}}/"
}
}

View File

@ -0,0 +1,3 @@
vars:pre-request {
base_url: https://brf.local
}

View File

@ -1,15 +0,0 @@
import { h } from 'preact'
import Head from './head.ts'
const OtherPage = () => (
<section>
<Head>
<title> : Other</title>
</Head>
<h1>Other Page</h1>
<p>Not the page where it begins</p>
</section>
)
export default OtherPage

View File

@ -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<Props> = ({ year }) => {
const [result, setResults] = useState([])
useEffect(() => {
rek(`/api/results/${year}`).then(setResults)
}, [year])
return (
<table>
<tbody>
{result.map((result) => (
<tr>
<td>{result.accountNumber}</td>
<td>{result.description}</td>
<td>{result.amount}</td>
</tr>
))}
</tbody>
</table>
)
}
export default Result

View File

@ -0,0 +1,5 @@
.years {
display: flex;
flex-direction: row-reverse;
justify-content: start;
}

View File

@ -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<number>(null)
useEffect(() => {
rek('/api/financial-years').then((financialYears) => {
setFinancialYears(financialYears)
setCurrentYear(financialYears[financialYears.length - 1].year)
})
}, [])
return (
<section>
<Head>
<title> : Results</title>
</Head>
<h1>Results</h1>
<div className={s.years}>
{financialYears.map((financialYear) => (
<button onClick={() => setCurrentYear(financialYear.year)}>{financialYear.year}</button>
))}
</div>
{currentYear ? (
<div>
<h2>{currentYear}</h2>
<Result year={currentYear} />
</div>
) : null}
</section>
)
}
export default ResultsPage

View File

@ -1,5 +1,5 @@
import Start from './components/start_page.tsx' import Start from './components/start_page.tsx'
import Other from './components/other_page.tsx' import Results from './components/results_page.tsx'
export default [ export default [
{ {
@ -9,9 +9,9 @@ export default [
component: Start, component: Start,
}, },
{ {
path: '/other', path: '/results',
name: 'other', name: 'results',
title: 'Other', title: 'Results',
component: Other, component: Results,
}, },
] ]

View File

@ -2,7 +2,7 @@
-- PostgreSQL database dump -- PostgreSQL database dump
-- --
\restrict EQkcX1mt4Oqej8UZL2Z1qyvnzIAf3O4TYefngsqIN91Lr6JLNTawzD4OIRSJr2s \restrict H2xBPQq6I6IQHZkAgiuKoY9lao4r4KAPtiyNvDE9oc0DN75cJ1gNbkoGFqutWDp
-- Dumped from database version 18.1 -- Dumped from database version 18.1
-- Dumped by pg_dump 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 ( CREATE TABLE public.financial_year (
id integer NOT NULL, id integer NOT NULL,
year integer NOT NULL,
start_date date NOT NULL, start_date date NOT NULL,
end_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); 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: - -- Name: journal journal_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
@ -486,5 +495,5 @@ ALTER TABLE ONLY public.transactions_to_objects
-- PostgreSQL database dump complete -- PostgreSQL database dump complete
-- --
\unrestrict EQkcX1mt4Oqej8UZL2Z1qyvnzIAf3O4TYefngsqIN91Lr6JLNTawzD4OIRSJr2s \unrestrict H2xBPQq6I6IQHZkAgiuKoY9lao4r4KAPtiyNvDE9oc0DN75cJ1gNbkoGFqutWDp

View File

@ -27,6 +27,7 @@
"@bmp/highlight-stack": "^0.1.2", "@bmp/highlight-stack": "^0.1.2",
"@fastify/middie": "^9.0.3", "@fastify/middie": "^9.0.3",
"@fastify/static": "^8.3.0", "@fastify/static": "^8.3.0",
"@fastify/type-provider-typebox": "^6.1.0",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"fastify": "^5.6.2", "fastify": "^5.6.2",
"fastify-plugin": "^5.1.0", "fastify-plugin": "^5.1.0",
@ -37,7 +38,8 @@
"pg-protocol": "^1.10.3", "pg-protocol": "^1.10.3",
"pino-abstract-transport": "^3.0.0", "pino-abstract-transport": "^3.0.0",
"preact": "^10.27.2", "preact": "^10.27.2",
"preact-router": "^4.1.2" "preact-router": "^4.1.2",
"rek": "^0.8.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.26.10", "@babel/core": "^7.26.10",
@ -53,6 +55,7 @@
"oxlint": "^1.29.0", "oxlint": "^1.29.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"sass": "^1.85.1", "sass": "^1.85.1",
"typebox": "^1.0.55",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vite": "^7.2.4" "vite": "^7.2.4"
} }

28
pnpm-lock.yaml generated
View File

@ -20,6 +20,9 @@ importers:
'@fastify/static': '@fastify/static':
specifier: ^8.3.0 specifier: ^8.3.0
version: 8.3.0 version: 8.3.0
'@fastify/type-provider-typebox':
specifier: ^6.1.0
version: 6.1.0(typebox@1.0.55)
chalk: chalk:
specifier: ^5.6.2 specifier: ^5.6.2
version: 5.6.2 version: 5.6.2
@ -53,6 +56,9 @@ importers:
preact-router: preact-router:
specifier: ^4.1.2 specifier: ^4.1.2
version: 4.1.2(preact@10.27.2) version: 4.1.2(preact@10.27.2)
rek:
specifier: ^0.8.1
version: 0.8.1
devDependencies: devDependencies:
'@babel/core': '@babel/core':
specifier: ^7.26.10 specifier: ^7.26.10
@ -93,6 +99,9 @@ importers:
sass: sass:
specifier: ^1.85.1 specifier: ^1.85.1
version: 1.94.2 version: 1.94.2
typebox:
specifier: ^1.0.55
version: 1.0.55
typescript: typescript:
specifier: ^5.8.2 specifier: ^5.8.2
version: 5.9.3 version: 5.9.3
@ -591,6 +600,11 @@ packages:
'@fastify/static@8.3.0': '@fastify/static@8.3.0':
resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==} 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': '@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@ -1844,6 +1858,9 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
rek@0.8.1:
resolution: {integrity: sha512-DXklCeA33/W9oaKjn/Upiwpdt7ZYqGJE2+1/l5v9iXwRzlfTEDtG7wMJLSWXftXgQz7/IlOjmLgOY/5OwbFPOA==}
require-from-string@2.0.2: require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2076,6 +2093,9 @@ packages:
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
engines: {node: '>=20'} engines: {node: '>=20'}
typebox@1.0.55:
resolution: {integrity: sha512-TP02wN0B6tDZngprrGVu/Z9s/QUyVEmR7VIg1yEOtsqyDdXXEoQPSfWdkD2PsA2lGLxu6GgwOTtGZVS9CAoERg==}
typescript@5.9.3: typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
@ -2604,6 +2624,10 @@ snapshots:
fastq: 1.19.1 fastq: 1.19.1
glob: 11.1.0 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/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0': '@isaacs/brace-expansion@5.0.0':
@ -3833,6 +3857,8 @@ snapshots:
gopd: 1.2.0 gopd: 1.2.0
set-function-name: 2.0.2 set-function-name: 2.0.2
rek@0.8.1: {}
require-from-string@2.0.2: {} require-from-string@2.0.2: {}
resolve-from@5.0.0: {} resolve-from@5.0.0: {}
@ -4080,6 +4106,8 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
typebox@1.0.55: {}
typescript@5.9.3: {} typescript@5.9.3: {}
undici-types@7.16.0: {} undici-types@7.16.0: {}

View File

@ -158,7 +158,11 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
if (yearNumber !== 0) continue 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 break
} }

View File

@ -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' 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<typeof FinancialYear>
const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
fastify.route({ fastify.route({
url: '/result', url: '/financial-years',
method: 'GET', 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 years = await knex('financialYear').select('*')
const accounts = await knex('account').select('*')
return Promise.all( return Promise.all(
years.map((year) => years.map((year) =>
knex('account') knex('account AS a')
.select('account.number', 'account.description') .select('a.number', 'a.description')
.sum('transaction.amount as amount') .sum('t.amount as amount')
.innerJoin('transaction', function () { .innerJoin('transaction AS t', function () {
this.on('transaction.accountNumber', '=', 'account.number') this.on('t.accountNumber', '=', 'a.number')
}) })
.innerJoin('entry', function () { .innerJoin('entry AS e', function () {
this.on('transaction.entryId', '=', 'entry.id') this.on('t.entryId', '=', 'e.id')
}) })
.groupBy('account.number', 'account.description') .groupBy('a.number', 'a.description')
.where('account.number', '>=', 3000) .where('a.number', '>=', 3000)
.where('entry.financialYearId', year.id) .where('e.financialYearId', year.id)
.orderBy('account.number') .orderBy('a.number')
.then((result) => ({ .then((result) => ({
startDate: year.startDate, startDate: year.startDate,
endDate: year.endDate, endDate: year.endDate,
result, result,
})), })),
), ),
) ).then((years) => ({
accounts,
years,
}))
}, },
}) })
fastify.route({ fastify.route({
url: '/result/:year', url: '/results/:year',
method: 'GET', method: 'GET',
async handler(req, res) { schema: {
const year = await knex('financialYear').first('*').where('startDate', `${req.params.year}0101`) 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') if (!year) return null
.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')
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')
}, },
}) })

View File

@ -1,4 +1,4 @@
import fastify from 'fastify' import fastify, { type FastifyServerOptions } from 'fastify'
import StatusError from './lib/status_error.ts' import StatusError from './lib/status_error.ts'
import env from './env.ts' import env from './env.ts'
import ErrorHandler from './handlers/error.ts' import ErrorHandler from './handlers/error.ts'
@ -6,7 +6,7 @@ import vitePlugin from './plugins/vite.ts'
import apiRoutes from './routes/api.ts' import apiRoutes from './routes/api.ts'
import templatePublic from './templates/public.ts' import templatePublic from './templates/public.ts'
export default async (options) => { export default async (options: FastifyServerOptions) => {
const server = fastify(options) const server = fastify(options)
server.setNotFoundHandler(() => { server.setNotFoundHandler(() => {