This commit is contained in:
Linus Miller 2025-11-26 14:22:27 +01:00
parent 347bce3fd6
commit ac61674b69
22 changed files with 847 additions and 39 deletions

15
.bruno/BRF/Invoice.bru Normal file
View File

@ -0,0 +1,15 @@
meta {
name: Invoice
type: http
seq: 7
}
get {
url: {{base_url}}/
body: none
auth: inherit
}
settings {
encodeUrl: true
}

15
.bruno/BRF/Invoices.bru Normal file
View File

@ -0,0 +1,15 @@
meta {
name: Invoices
type: http
seq: 6
}
get {
url: {{base_url}}/api/invoices
body: none
auth: inherit
}
settings {
encodeUrl: true
}

View File

@ -0,0 +1,15 @@
meta {
name: /api/suppliers
type: http
seq: 8
}
get {
url: {{base_url}}/
body: none
auth: inherit
}
settings {
encodeUrl: true
}

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
/dump
/sie
/invoices
/uploads
# Logs
logs

View File

@ -0,0 +1,64 @@
import fs from 'node:fs/promises'
import { existsSync } from 'node:fs'
import path from 'node:path'
import knex from '../server/lib/knex.ts'
const dirs = process.argv.slice(2)
// 172-972, 2020-01-08, Great Security Sverige AB.pdf'
const rFileName = /^172-(\d+),\s(\d{4,4}-\d{2,2}-\d{2,2}), (.*)\.pdf$/
for await (const dir of dirs) {
await readdir(dir)
}
knex.destroy()
async function readdir(dir: string) {
const files = (await fs.readdir(dir)).toSorted((a: string, b: string) => {
const [, aNum] = a.match(rFileName)
const [, bNum] = b.match(rFileName)
if (parseInt(aNum) > parseInt(bNum)) {
return 1
} else {
return -1
}
})
const trx = await knex.transaction()
for await (const originalFilename of files) {
const result = originalFilename.match(rFileName)
if (!result) {
throw new Error(originalFilename)
}
const [, fiskenNumber, invoiceDate, supplierName] = result
let supplier = await trx('supplier').first('*').where('name', supplierName)
if (!supplier) {
supplier = (await trx('supplier').insert({ name: supplierName, supplierTypeId: 1 }).returning('*'))[0]
}
const invoice = (
await trx('invoice').insert({ fiskenNumber, invoiceDate, supplierId: supplier.id }).returning('*')
)[0]
const ext = path.extname(originalFilename)
const filename = `${invoiceDate}_fisken_${fiskenNumber}_${supplierName.split(/[\s/]/).join('_').split(/[']/).join('')}${ext}`
const pathname = path.join('uploads', 'invoices', filename)
if (!existsSync(pathname)) {
console.info(filename)
await fs.copyFile(path.join(dir, originalFilename), pathname)
}
const file = (await trx('file').insert({ filename }).returning('id'))[0]
await trx('filesToInvoice').insert({ fileId: file.id, invoiceId: invoice.id })
}
await trx.commit()
}

73
bin/add_phm_invoices.ts Normal file
View File

@ -0,0 +1,73 @@
import fs from 'node:fs/promises'
import { existsSync } from 'node:fs'
import path from 'node:path'
import knex from '../server/lib/knex.ts'
import split from '../server/lib/split.ts'
import { csvParseRows } from 'd3-dsv'
const csvFilename = process.argv[2]
const csvString = await fs.readFile(csvFilename, { encoding: 'utf8' })
const rows = csvParseRows(csvString)
const trx = await knex.transaction()
for (const row of rows.toReversed()) {
const [
phmNumber,
type,
supplierId,
supplierName,
invoiceDate,
dueDate,
invoiceNumber,
ocr,
amount,
vat,
balance,
currency,
status,
filesString,
] = row
let supplier = await trx('supplier').first('*').where('name', supplierName)
if (!supplier) {
supplier = (await trx('supplier').insert({ name: supplierName, supplierTypeId: 1 }).returning('*'))[0]
}
const invoice = (
await trx('invoice')
.insert({
invoiceDate,
supplierId: supplier.id,
dueDate,
ocr,
invoiceNumber,
phmNumber,
amount,
})
.returning('id')
)[0]
const filenames = filesString.split(',').map((filename) => filename.trim())
// TODO handle names if multiple files with same extension (otherwise they will have the same name)
for (const originalFilename of filenames) {
const ext = path.extname(originalFilename)
const filename = `${invoiceDate}_phm_${phmNumber}_${supplierName.split(/[\s/]/).join('_').split(/[']/).join('')}${ext}`
const pathname = path.join('uploads', 'invoices', filename)
if (!existsSync(pathname)) {
console.info(filename)
await fs.copyFile(path.join('invoices', 'phm', originalFilename), pathname)
}
const file = (await trx('file').insert({ filename }).returning('id'))[0]
trx('filesToInvoice').insert({ fileId: file.id, invoiceId: invoice.id })
}
}
trx.commit()
knex.destroy()

View File

@ -8,6 +8,8 @@ for await (const file of process.argv.slice(2)) {
console.log(`- parsing file: ${file}`)
await parseStream(fh.readableWebStream())
await fh.close()
}
knex.destroy()

View File

@ -1,5 +1,5 @@
import { h } from 'preact'
import { Router } from 'preact-router'
import { LocationProvider, Route, Router } from 'preact-iso'
import Head from './head.ts'
import Footer from './footer.tsx'
import Header from './header.tsx'
@ -10,27 +10,28 @@ import s from './app.module.scss'
export default function App({ error, url, title }) {
return (
<div id='app' className={s.base}>
<Head>
<title>{title || 'Untitled'}</title>
</Head>
<LocationProvider>
<div id='app' className={s.base}>
<Head>
<title>{title || 'Untitled'}</title>
</Head>
<Header routes={routes} />
<Header routes={routes} />
<main className={s.main}>
{error ? (
<ErrorPage error={error} />
) : (
<Router url={url}>
{routes.map((route) => (
// @ts-ignore
<route.component key={route.path} path={route.path} route={route} />
))}
</Router>
)}
</main>
<main className={s.main}>
{error ? (
<ErrorPage error={error} />
) : (
<Router>
{routes.map((route) => (
<Route key={route.path} route={route} path={route.path} component={route.component} />
))}
</Router>
)}
</main>
<Footer />
</div>
<Footer />
</div>
</LocationProvider>
)
}

View File

@ -5,11 +5,13 @@ const Header = ({ routes }) => (
<h1>BRF Tegeltrasten</h1>
<nav>
<ul>
{routes.map((route) => (
<li key={route.path}>
<a href={route.path}>{route.title}</a>
</li>
))}
{routes.map((route) =>
route.nav !== false ? (
<li key={route.path}>
<a href={route.path}>{route.title}</a>
</li>
) : null,
)}
</ul>
</nav>
</header>

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 Invoices: FunctionalComponent<Props> = ({ year }) => {
const [invoices, setInvoices] = useState([])
useEffect(() => {
rek(`/api/invoices/by-year/${year}`).then(setInvoices)
}, [year])
return (
<table>
<tbody>
{invoices.map((invoice) => (
<tr>
<td>{invoice.accountNumber}</td>
<td>{invoice.description}</td>
<td>{invoice.amount}</td>
</tr>
))}
</tbody>
</table>
)
}
export default Invoices

View File

View File

@ -0,0 +1,65 @@
import { h } from 'preact'
import Format from 'easy-tz/format'
import { useEffect, useState } from 'preact/hooks'
import { useRoute } from 'preact-iso'
import rek from 'rek'
import Head from './head.ts'
const format = Format.bind(null, null, 'YYYY.MM.DD')
const InvoicesPage = () => {
const [supplier, setSupplier] = useState(null)
const [invoices, setInvoices] = useState([])
const route = useRoute()
useEffect(() => {
rek(`/api/suppliers/${route.params.supplier}`).then((supplier) => setSupplier(supplier))
rek(`/api/invoices?supplier=${route.params.supplier}`).then((invoices) => setInvoices(invoices))
}, [route.params.supplier])
return (
<section>
<Head>
<title> : Invoices : {supplier?.name}</title>
</Head>
<h1>Invoices for {supplier?.name}</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Fisken</th>
<th>PHM</th>
<th>Invoice Date</th>
<th>Due Date</th>
<th>Number</th>
<th>Amount</th>
<th>Files</th>
</tr>
</thead>
<tbody>
{invoices.map((invoice) => (
<tr>
<td>{invoice.id}</td>
<td>{invoice.fiskenNumber}</td>
<td>{invoice.phmNumber}</td>
<td>{format(invoice.invoiceDate)}</td>
<td>{invoice.dueDate && format(invoice.dueDate)}</td>
<td>{invoice.invoiceNumber}</td>
<td>{invoice.amount}</td>
<td>
{invoice.files?.map((file) => (
<a href={`/uploads/invoices/${file.filename}`}>{file.filename}</a>
))}
</td>
</tr>
))}
</tbody>
</table>
</section>
)
}
export default InvoicesPage

View File

@ -0,0 +1,39 @@
import { h } from 'preact'
import { useEffect, useState } from 'preact/hooks'
import rek from 'rek'
import Head from './head.ts'
import Invoice from './invoice.tsx'
const InvoicesPage = () => {
const [suppliers, setSuppliers] = useState([])
const [financialYears, setFinancialYears] = useState([])
const [currentYear, setCurrentYear] = useState<number>(null)
useEffect(() => {
rek('/api/suppliers').then((suppliers) => setSuppliers(suppliers))
// rek('/api/financial-years').then((financialYears) => {
// setFinancialYears(financialYears)
// setCurrentYear(financialYears[financialYears.length - 1].year)
// })
}, [])
console.log(suppliers)
return (
<section>
<Head>
<title> : Invoices</title>
</Head>
<h1>Invoices</h1>
<ul>
{suppliers?.map((supplier) => (
<li>
<a href={`/invoices/by-supplier/${supplier.id}`}>{supplier.name}</a>
</li>
))}
</ul>
</section>
)
}
export default InvoicesPage

View File

@ -1,4 +1,6 @@
import Start from './components/start_page.tsx'
import Invoices from './components/invoices_page.tsx'
import InvoicesBySupplier from './components/invoices_by_supplier_page.tsx'
import Objects from './components/objects_page.tsx'
import Results from './components/results_page.tsx'
@ -15,6 +17,19 @@ export default [
title: 'Objects',
component: Objects,
},
{
path: '/invoices',
name: 'invoices',
title: 'Invoices',
component: Invoices,
},
{
path: '/invoices/by-supplier/:supplier',
name: 'invoices',
title: 'Invoices',
nav: false,
component: InvoicesBySupplier,
},
{
path: '/results',
name: 'results',

View File

@ -17,6 +17,7 @@ services:
volumes:
- ./client:/home/node/brf_books/client
- ./server:/home/node/brf_books/server
- ./uploads:/home/node/brf_books/uploads
depends_on:
- postgres
- redis

View File

@ -2,7 +2,7 @@
-- PostgreSQL database dump
--
\restrict H2xBPQq6I6IQHZkAgiuKoY9lao4r4KAPtiyNvDE9oc0DN75cJ1gNbkoGFqutWDp
\restrict x1LNwo1MrXgJU7KVXELaKlpEPpIZLjRGuaweMIify4ofZcwqTGzXVX5DkRI11Hx
-- Dumped from database version 18.1
-- Dumped by pg_dump version 18.1
@ -156,6 +156,46 @@ CREATE SEQUENCE public.entry_id_seq
ALTER SEQUENCE public.entry_id_seq OWNED BY public.entry.id;
--
-- Name: file; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.file (
id integer NOT NULL,
filename text CONSTRAINT file_file_not_null NOT NULL
);
--
-- Name: file_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.file_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: file_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.file_id_seq OWNED BY public.file.id;
--
-- Name: files_to_invoice; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.files_to_invoice (
invoice_id integer NOT NULL,
file_id integer NOT NULL
);
--
-- Name: financial_year; Type: TABLE; Schema: public; Owner: -
--
@ -188,6 +228,44 @@ CREATE SEQUENCE public.financial_year_id_seq
ALTER SEQUENCE public.financial_year_id_seq OWNED BY public.financial_year.id;
--
-- Name: invoice; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.invoice (
id integer NOT NULL,
financial_year_id integer,
supplier_id integer NOT NULL,
fisken_number integer,
phm_number integer,
invoice_number text,
invoice_date date CONSTRAINT invoice_date_not_null NOT NULL,
due_date date,
ocr text,
amount numeric(12,2)
);
--
-- Name: invoice_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.invoice_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: invoice_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.invoice_id_seq OWNED BY public.invoice.id;
--
-- Name: journal; Type: TABLE; Schema: public; Owner: -
--
@ -251,6 +329,67 @@ CREATE SEQUENCE public.object_id_seq
ALTER SEQUENCE public.object_id_seq OWNED BY public.object.id;
--
-- Name: supplier; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.supplier (
id integer NOT NULL,
name text,
supplier_type_id integer NOT NULL
);
--
-- Name: supplier_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.supplier_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: supplier_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.supplier_id_seq OWNED BY public.supplier.id;
--
-- Name: supplier_type; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.supplier_type (
id integer NOT NULL,
name text NOT NULL
);
--
-- Name: supplier_type_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.supplier_type_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: supplier_type_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.supplier_type_id_seq OWNED BY public.supplier_type.id;
--
-- Name: transaction; Type: TABLE; Schema: public; Owner: -
--
@ -264,7 +403,8 @@ CREATE TABLE public.transaction (
description text,
transaction_date date,
quantity numeric(12,2),
signature text
signature text,
invoice_id integer
);
@ -319,6 +459,13 @@ ALTER TABLE ONLY public.dimension ALTER COLUMN id SET DEFAULT nextval('public.di
ALTER TABLE ONLY public.entry ALTER COLUMN id SET DEFAULT nextval('public.entry_id_seq'::regclass);
--
-- Name: file id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.file ALTER COLUMN id SET DEFAULT nextval('public.file_id_seq'::regclass);
--
-- Name: financial_year id; Type: DEFAULT; Schema: public; Owner: -
--
@ -326,6 +473,13 @@ ALTER TABLE ONLY public.entry ALTER COLUMN id SET DEFAULT nextval('public.entry_
ALTER TABLE ONLY public.financial_year ALTER COLUMN id SET DEFAULT nextval('public.financial_year_id_seq'::regclass);
--
-- Name: invoice id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.invoice ALTER COLUMN id SET DEFAULT nextval('public.invoice_id_seq'::regclass);
--
-- Name: journal id; Type: DEFAULT; Schema: public; Owner: -
--
@ -340,6 +494,20 @@ ALTER TABLE ONLY public.journal ALTER COLUMN id SET DEFAULT nextval('public.jour
ALTER TABLE ONLY public.object ALTER COLUMN id SET DEFAULT nextval('public.object_id_seq'::regclass);
--
-- Name: supplier id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.supplier ALTER COLUMN id SET DEFAULT nextval('public.supplier_id_seq'::regclass);
--
-- Name: supplier_type id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.supplier_type ALTER COLUMN id SET DEFAULT nextval('public.supplier_type_id_seq'::regclass);
--
-- Name: transaction id; Type: DEFAULT; Schema: public; Owner: -
--
@ -387,6 +555,22 @@ ALTER TABLE ONLY public.entry
ADD CONSTRAINT entry_pkey PRIMARY KEY (id);
--
-- Name: file file_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.file
ADD CONSTRAINT file_pkey PRIMARY KEY (id);
--
-- Name: files_to_invoice files_to_invoice_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.files_to_invoice
ADD CONSTRAINT files_to_invoice_pkey PRIMARY KEY (invoice_id, file_id);
--
-- Name: financial_year financial_year_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -411,6 +595,14 @@ ALTER TABLE ONLY public.financial_year
ADD CONSTRAINT financial_year_year_key UNIQUE (year);
--
-- Name: invoice invoice_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.invoice
ADD CONSTRAINT invoice_pkey PRIMARY KEY (id);
--
-- Name: journal journal_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -435,6 +627,22 @@ ALTER TABLE ONLY public.object
ADD CONSTRAINT object_pkey PRIMARY KEY (id);
--
-- Name: supplier supplier_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.supplier
ADD CONSTRAINT supplier_pkey PRIMARY KEY (id);
--
-- Name: supplier_type supplier_type_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.supplier_type
ADD CONSTRAINT supplier_type_pkey PRIMARY KEY (id);
--
-- Name: transaction transaction_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -451,6 +659,38 @@ ALTER TABLE ONLY public.transactions_to_objects
ADD CONSTRAINT transactions_to_objects_transaction_id_object_id_key UNIQUE (transaction_id, object_id);
--
-- Name: files_to_invoice files_to_invoice_file_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.files_to_invoice
ADD CONSTRAINT files_to_invoice_file_id_fkey FOREIGN KEY (file_id) REFERENCES public.file(id);
--
-- Name: files_to_invoice files_to_invoice_invoice_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.files_to_invoice
ADD CONSTRAINT files_to_invoice_invoice_id_fkey FOREIGN KEY (invoice_id) REFERENCES public.invoice(id);
--
-- Name: invoice invoice_financial_year_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.invoice
ADD CONSTRAINT invoice_financial_year_id_fkey FOREIGN KEY (financial_year_id) REFERENCES public.financial_year(id);
--
-- Name: invoice invoice_supplier_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.invoice
ADD CONSTRAINT invoice_supplier_id_fkey FOREIGN KEY (supplier_id) REFERENCES public.supplier(id);
--
-- Name: object object_dimension_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -459,6 +699,14 @@ ALTER TABLE ONLY public.object
ADD CONSTRAINT object_dimension_id_fkey FOREIGN KEY (dimension_id) REFERENCES public.dimension(id);
--
-- Name: supplier supplier_supplier_type_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.supplier
ADD CONSTRAINT supplier_supplier_type_id_fkey FOREIGN KEY (supplier_type_id) REFERENCES public.supplier_type(id);
--
-- Name: transaction transaction_entry_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -467,6 +715,14 @@ ALTER TABLE ONLY public.transaction
ADD CONSTRAINT transaction_entry_id_fkey FOREIGN KEY (entry_id) REFERENCES public.entry(id);
--
-- Name: transaction transaction_invoice_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.transaction
ADD CONSTRAINT transaction_invoice_id_fkey FOREIGN KEY (invoice_id) REFERENCES public.invoice(id);
--
-- Name: transaction transaction_object_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -495,5 +751,5 @@ ALTER TABLE ONLY public.transactions_to_objects
-- PostgreSQL database dump complete
--
\unrestrict H2xBPQq6I6IQHZkAgiuKoY9lao4r4KAPtiyNvDE9oc0DN75cJ1gNbkoGFqutWDp
\unrestrict x1LNwo1MrXgJU7KVXELaKlpEPpIZLjRGuaweMIify4ofZcwqTGzXVX5DkRI11Hx

View File

@ -0,0 +1,44 @@
--
-- PostgreSQL database dump
--
\restrict bwzPcLzyW22uigCF5UMc3FAcSU9hliUs5ZBJ4ZpwZeegYAG8Md8d7l0M55Czl7h
-- Dumped from database version 18.1
-- Dumped by pg_dump version 18.1
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET transaction_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Data for Name: supplier_type; Type: TABLE DATA; Schema: public; Owner: -
--
COPY public.supplier_type (id, name) FROM stdin;
1 Company
2 Person
\.
--
-- Name: supplier_type_id_seq; Type: SEQUENCE SET; Schema: public; Owner: -
--
SELECT pg_catalog.setval('public.supplier_type_id_seq', 2, true);
--
-- PostgreSQL database dump complete
--
\unrestrict bwzPcLzyW22uigCF5UMc3FAcSU9hliUs5ZBJ4ZpwZeegYAG8Md8d7l0M55Czl7h

View File

@ -20,7 +20,7 @@
"test": "pnpm run test:client && pnpm run test:server",
"test:client": "node --no-warnings --import=./client/test/jsdom_polyfills.ts --import=./client/test/register_tsx_hook.ts --test ./client/**/*.test.ts{,x}",
"test:server": "node --no-warnings --test ./server/**/*.test.ts",
"types": "tsgo"
"types": "tsgo --skipLibCheck"
},
"dependencies": {
"@bmp/console": "^0.1.0",
@ -29,6 +29,7 @@
"@fastify/static": "^8.3.0",
"@fastify/type-provider-typebox": "^6.1.0",
"chalk": "^5.6.2",
"easy-tz": "^0.2.0",
"fastify": "^5.6.2",
"fastify-plugin": "^5.1.0",
"knex": "^3.1.0",
@ -38,7 +39,7 @@
"pg-protocol": "^1.10.3",
"pino-abstract-transport": "^3.0.0",
"preact": "^10.27.2",
"preact-router": "^4.1.2",
"preact-iso": "^2.11.0",
"rek": "^0.8.1"
},
"devDependencies": {
@ -48,6 +49,7 @@
"@types/lodash": "^4.17.16",
"@types/node": "^24.10.1",
"@typescript/native-preview": "7.0.0-dev.20251126.1",
"d3-dsv": "^3.0.1",
"esbuild": "^0.27.0",
"globals": "^16.0.0",
"husky": "^9.1.7",

58
pnpm-lock.yaml generated
View File

@ -26,6 +26,9 @@ importers:
chalk:
specifier: ^5.6.2
version: 5.6.2
easy-tz:
specifier: ^0.2.0
version: 0.2.0
fastify:
specifier: ^5.6.2
version: 5.6.2
@ -53,9 +56,9 @@ importers:
preact:
specifier: ^10.27.2
version: 10.27.2
preact-router:
specifier: ^4.1.2
version: 4.1.2(preact@10.27.2)
preact-iso:
specifier: ^2.11.0
version: 2.11.0(preact-render-to-string@6.6.3(preact@10.27.2))(preact@10.27.2)
rek:
specifier: ^0.8.1
version: 0.8.1
@ -78,6 +81,9 @@ importers:
'@typescript/native-preview':
specifier: 7.0.0-dev.20251126.1
version: 7.0.0-dev.20251126.1
d3-dsv:
specifier: ^3.0.1
version: 3.0.1
esbuild:
specifier: ^0.27.0
version: 0.27.0
@ -1114,6 +1120,10 @@ packages:
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
engines: {node: '>=20'}
commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
content-disposition@0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
@ -1144,6 +1154,11 @@ packages:
resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==}
engines: {node: '>=20'}
d3-dsv@3.0.1:
resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
engines: {node: '>=12'}
hasBin: true
data-urls@6.0.0:
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
engines: {node: '>=20'}
@ -1217,6 +1232,9 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
easy-tz@0.2.0:
resolution: {integrity: sha512-Mf+tTNHaAUBqEMo9mvIdCIV5kXCazRCxGKH16itMxefrHy/FtHS5tnfOOS0DKMWU7rl/MnN9PzjdFfn8JCYqxg==}
electron-to-chromium@1.5.259:
resolution: {integrity: sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==}
@ -1848,10 +1866,16 @@ packages:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
preact-router@4.1.2:
resolution: {integrity: sha512-uICUaUFYh+XQ+6vZtQn1q+X6rSqwq+zorWOCLWPF5FAsQh3EJ+RsDQ9Ee+fjk545YWQHfUxhrBAaemfxEnMOUg==}
preact-iso@2.11.0:
resolution: {integrity: sha512-oThWJQcgcnaWh6UKy1qrBkxIWp5CkqvnHiFdLiDUxfNkGdpQ5veGQw9wOVS0NDp7X8xo98wxE4wng5jLv1e9Ug==}
peerDependencies:
preact: '>=10'
preact: '>=10 || >= 11.0.0-0'
preact-render-to-string: '>=6.4.0'
preact-render-to-string@6.6.3:
resolution: {integrity: sha512-7oHG7jzjriqsFPkSPiPnzrQ0GcxFm6wOkYWNdStK5Ks9YlWSQQXKGBRAX4nKDdqX7HAQuRvI4pZNZMycK4WwDw==}
peerDependencies:
preact: '>=10 || >= 11.0.0-0'
preact@10.27.2:
resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==}
@ -1933,6 +1957,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
rw@1.3.3:
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@ -3093,6 +3120,8 @@ snapshots:
commander@14.0.2: {}
commander@7.2.0: {}
content-disposition@0.5.4:
dependencies:
safe-buffer: 5.2.1
@ -3128,6 +3157,12 @@ snapshots:
'@csstools/css-syntax-patches-for-csstree': 1.0.17
css-tree: 3.1.0
d3-dsv@3.0.1:
dependencies:
commander: 7.2.0
iconv-lite: 0.6.3
rw: 1.3.3
data-urls@6.0.0:
dependencies:
whatwg-mimetype: 4.0.0
@ -3211,6 +3246,8 @@ snapshots:
eastasianwidth@0.2.0: {}
easy-tz@0.2.0: {}
electron-to-chromium@1.5.259: {}
emoji-regex@10.6.0: {}
@ -3881,7 +3918,12 @@ snapshots:
dependencies:
xtend: 4.0.2
preact-router@4.1.2(preact@10.27.2):
preact-iso@2.11.0(preact-render-to-string@6.6.3(preact@10.27.2))(preact@10.27.2):
dependencies:
preact: 10.27.2
preact-render-to-string: 6.6.3(preact@10.27.2)
preact-render-to-string@6.6.3(preact@10.27.2):
dependencies:
preact: 10.27.2
@ -3973,6 +4015,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.53.3
fsevents: 2.3.3
rw@1.3.3: {}
safe-buffer@5.2.1: {}
safe-regex-test@1.1.0:

View File

@ -1,3 +1,4 @@
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'
@ -19,6 +20,102 @@ const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
},
})
fastify.route({
url: '/invoices',
method: 'GET',
schema: {
querystring: Type.Object({
year: Type.Optional(Type.Number()),
supplier: Type.Optional(Type.Number()),
}),
},
async handler(req) {
let query: { financialYearId?: number; supplierId?: 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.supplier) {
query.supplierId = req.query.supplier
}
return knex('invoice AS i')
.select('*', {
files: knex
.select(knex.raw('json_agg(files)'))
.from(
knex
.select('id', 'filename')
.from('file AS f')
.innerJoin('filesToInvoice AS fi', 'f.id', 'fi.fileId')
.where('fi.invoiceId', knex.ref('i.id'))
.as('files'),
),
})
.where(query)
},
})
fastify.route({
url: '/invoices/:id',
method: 'GET',
schema: {
params: Type.Object({
id: Type.Number(),
}),
},
handler(req) {
return knex('invoice').first('*').where('id', req.params.id)
},
})
fastify.route({
url: '/invoices/by-supplier/:supplier',
method: 'GET',
schema: {
params: Type.Object({
supplier: Type.Number(),
}),
},
handler(req) {
return knex('invoice AS i')
.select('*', {
files: knex
.select(knex.raw('json_agg(files)'))
.from(
knex
.select('id', 'filename')
.from('file AS f')
.innerJoin('filesToInvoice AS fi', 'f.id', 'fi.fileId')
.where('fi.invoiceId', knex.ref('i.id'))
.as('files'),
),
})
.where('supplierId', req.params.supplier)
},
})
fastify.route({
url: '/invoices/by-year/:year',
method: 'GET',
schema: {
params: Type.Object({
year: Type.Number(),
}),
},
async handler(req) {
const year = await knex('financialYear').first('*').where('year', req.params.year)
if (!year) throw new StatusError(404, `Year ${req.params.year} not found.`)
return knex('invoice').select('*').where('financialYearId', year.id)
},
})
fastify.route({
url: '/objects',
method: 'GET',
@ -122,6 +219,27 @@ const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
},
})
fastify.route({
url: '/suppliers',
method: 'GET',
async handler(req) {
return knex('supplier').select('*').orderBy('name')
},
})
fastify.route({
url: '/suppliers/:id',
method: 'GET',
schema: {
params: Type.Object({
id: Type.Number(),
}),
},
async handler(req) {
return knex('supplier').first('*').where('id', req.params.id)
},
})
done()
}

View File

@ -1,4 +1,5 @@
import fastify, { type FastifyServerOptions } from 'fastify'
import fstatic from '@fastify/static'
import StatusError from './lib/status_error.ts'
import env from './env.ts'
import ErrorHandler from './handlers/error.ts'
@ -13,7 +14,10 @@ export default async (options: FastifyServerOptions) => {
throw new StatusError(404)
})
console.dir(env)
server.register(fstatic, {
root: new URL('../uploads', import.meta.url),
prefix: '/uploads/',
})
server.register(vitePlugin, {
mode: env.NODE_ENV,

View File

@ -12,5 +12,5 @@
"erasableSyntaxOnly": true,
"allowArbitraryExtensions": true
},
"include": ["global.d.ts", "client", "server"]
"include": ["global.d.ts", "bin", "client", "server"]
}