Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b08aca3732 | |||
| e58e589f8e | |||
| 794d81438d | |||
| a17baba9c9 | |||
| bcc02fbd78 |
20
.bruno/API/api-accounts--number.bru
Normal file
20
.bruno/API/api-accounts--number.bru
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
meta {
|
||||||
|
name: /api/accounts/:number
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_url}}/api/accounts/:number
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
params:path {
|
||||||
|
number: 3011
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
20
.bruno/API/api-accounts-month-sum--number.bru
Normal file
20
.bruno/API/api-accounts-month-sum--number.bru
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
meta {
|
||||||
|
name: /api/accounts/month-sum/:number
|
||||||
|
type: http
|
||||||
|
seq: 24
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_url}}/api/accounts/month-sum/:number
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
params:path {
|
||||||
|
number: 3011
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
20
.bruno/API/api-accounts-year-sum--number.bru
Normal file
20
.bruno/API/api-accounts-year-sum--number.bru
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
meta {
|
||||||
|
name: /api/accounts/year-sum/:number
|
||||||
|
type: http
|
||||||
|
seq: 25
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_url}}/api/accounts/year-sum/:number
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
params:path {
|
||||||
|
number: 3011
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/balances
|
name: /api/balances
|
||||||
type: http
|
type: http
|
||||||
seq: 3
|
seq: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/entries/:id
|
name: /api/entries/:id
|
||||||
type: http
|
type: http
|
||||||
seq: 5
|
seq: 6
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/entries
|
name: /api/entries
|
||||||
type: http
|
type: http
|
||||||
seq: 4
|
seq: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/errors
|
name: /api/errors
|
||||||
type: http
|
type: http
|
||||||
seq: 22
|
seq: 23
|
||||||
}
|
}
|
||||||
|
|
||||||
delete {
|
delete {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/financial-years
|
name: /api/financial-years
|
||||||
type: http
|
type: http
|
||||||
seq: 6
|
seq: 7
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/invites
|
name: /api/invites
|
||||||
type: http
|
type: http
|
||||||
seq: 10
|
seq: 11
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/invoices/:id
|
name: /api/invoices/:id
|
||||||
type: http
|
type: http
|
||||||
seq: 8
|
seq: 9
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/invoices/total-amount
|
name: /api/invoices/total-amount
|
||||||
type: http
|
type: http
|
||||||
seq: 9
|
seq: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/invoices
|
name: /api/invoices
|
||||||
type: http
|
type: http
|
||||||
seq: 7
|
seq: 8
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/journals
|
name: /api/journals
|
||||||
type: http
|
type: http
|
||||||
seq: 21
|
seq: 22
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/objects/:id/transactions
|
name: /api/objects/:id/transactions
|
||||||
type: http
|
type: http
|
||||||
seq: 12
|
seq: 13
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/objects
|
name: /api/objects
|
||||||
type: http
|
type: http
|
||||||
seq: 11
|
seq: 12
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/results/:year
|
name: /api/results/:year
|
||||||
type: http
|
type: http
|
||||||
seq: 14
|
seq: 15
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/results
|
name: /api/results
|
||||||
type: http
|
type: http
|
||||||
seq: 13
|
seq: 14
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/roles
|
name: /api/roles
|
||||||
type: http
|
type: http
|
||||||
seq: 15
|
seq: 16
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/suppliers/:id
|
name: /api/suppliers/:id
|
||||||
type: http
|
type: http
|
||||||
seq: 17
|
seq: 18
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/suppliers/merge
|
name: /api/suppliers/merge
|
||||||
type: http
|
type: http
|
||||||
seq: 18
|
seq: 19
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/suppliers
|
name: /api/suppliers
|
||||||
type: http
|
type: http
|
||||||
seq: 16
|
seq: 17
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/transactions
|
name: /api/transactions
|
||||||
type: http
|
type: http
|
||||||
seq: 19
|
seq: 20
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/users
|
name: /api/users
|
||||||
type: http
|
type: http
|
||||||
seq: 20
|
seq: 21
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
|
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
|
||||||
"oxlint --fix",
|
"oxlint --fix",
|
||||||
"prettier --write"
|
"oxfmt"
|
||||||
],
|
],
|
||||||
"*.{css,scss,json}": [
|
"*.{css,scss,json}": [
|
||||||
"prettier --write"
|
"oxfmt"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
dist
|
|
||||||
dump
|
|
||||||
*.*
|
|
||||||
!*.cjs
|
|
||||||
!*.css
|
|
||||||
!*.cts
|
|
||||||
!*.html
|
|
||||||
!*.js
|
|
||||||
!*.json
|
|
||||||
!*.jsx
|
|
||||||
!*.mjs
|
|
||||||
!*.mts
|
|
||||||
!*.scss
|
|
||||||
!*.ts
|
|
||||||
!*.tsx
|
|
||||||
!*.yaml
|
|
||||||
!*.yml
|
|
||||||
pnpm-lock.yaml
|
|
||||||
17
.prettierrc
17
.prettierrc
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"printWidth": 120,
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"embeddedLanguageFormatting": "off",
|
|
||||||
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": "*.scss",
|
|
||||||
"options": {
|
|
||||||
"trailingComma": "none"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -65,7 +65,7 @@ async function readdir(dir: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const file = await trx.insertInto('file').values({ filename }).returning('id').executeTakeFirstOrThrow()
|
const file = await trx.insertInto('file').values({ filename }).returning('id').executeTakeFirstOrThrow()
|
||||||
await trx.insertInto('filesToInvoice').values({ fileId: file.id, invoiceId: invoice.id }).execute()
|
await trx.insertInto('files_invoice').values({ fileId: file.id, invoiceId: invoice.id }).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
await trx.commit().execute()
|
await trx.commit().execute()
|
||||||
|
|||||||
@ -68,7 +68,7 @@ for (const row of rows.toReversed()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const file = await trx.insertInto('file').values({ filename }).returning('id').executeTakeFirstOrThrow()
|
const file = await trx.insertInto('file').values({ filename }).returning('id').executeTakeFirstOrThrow()
|
||||||
await trx.insertInto('filesToInvoice').values({ fileId: file.id, invoiceId: invoice.id }).execute()
|
await trx.insertInto('files_invoice').values({ fileId: file.id, invoiceId: invoice.id }).execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { h, type FunctionComponent } from 'preact'
|
|||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
import { Router } from 'preact-router'
|
import { Router } from 'preact-router'
|
||||||
|
|
||||||
import CurrentUserContext from '../contexts/current_user.ts'
|
import CurrentUserContext from '../../shared/contexts/current_user.ts'
|
||||||
import { NotificationsProvider } from '../contexts/notifications.tsx'
|
import { NotificationsProvider } from '../contexts/notifications.tsx'
|
||||||
import routes from '../routes.ts'
|
import routes from '../routes.ts'
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ const App: FunctionComponent<{ state: ANY }> = ({ state }) => {
|
|||||||
<CurrentUserContext.Provider value={{ user, setUser }}>
|
<CurrentUserContext.Provider value={{ user, setUser }}>
|
||||||
<div className={s.base}>
|
<div className={s.base}>
|
||||||
<a href='/admin' className={s.logo}>
|
<a href='/admin' className={s.logo}>
|
||||||
Carson Admin
|
BRF Admin
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<header className={s.header}>
|
<header className={s.header}>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { h, type FunctionComponent } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
|
||||||
import { useCurrentUser } from '../contexts/current_user.ts'
|
import { useCurrentUser } from '../../shared/contexts/current_user.ts'
|
||||||
import s from './current_user.module.scss'
|
import s from './current_user.module.scss'
|
||||||
|
|
||||||
const CurrentUser: FunctionComponent<{ className?: string }> = ({ className }) => {
|
const CurrentUser: FunctionComponent<{ className?: string }> = ({ className }) => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { h, type FunctionComponent, type TargetedSubmitEvent } from 'preact'
|
import { h, type FunctionComponent, type TargetedSubmitEvent } from 'preact'
|
||||||
import type { FetchError } from 'rek'
|
import type { FetchError } from 'rek'
|
||||||
import { useCallback } from 'preact/hooks'
|
import { useCallback } from 'preact/hooks'
|
||||||
import { useCurrentUser } from '../contexts/current_user.ts'
|
import { useCurrentUser } from '../../shared/contexts/current_user.ts'
|
||||||
import rek from 'rek'
|
import rek from 'rek'
|
||||||
|
|
||||||
import useRequestState from '../../shared/hooks/use_request_state.ts'
|
import useRequestState from '../../shared/hooks/use_request_state.ts'
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { h } from 'preact'
|
import { h } from 'preact'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import { route } from 'preact-router'
|
import { route } from 'preact-router'
|
||||||
import { useCurrentUser } from '../contexts/current_user.ts'
|
import { useCurrentUser } from '../../shared/contexts/current_user.ts'
|
||||||
|
|
||||||
/** @type {import('preact').FunctionComponent<{ auth: boolean, path: string, component: () => any, loadComponent: boolean}} Page */
|
/** @type {import('preact').FunctionComponent<{ auth: boolean, path: string, component: () => any, loadComponent: boolean}} Page */
|
||||||
const Route = ({ auth, path, component, loadComponent = true }) => {
|
const Route = ({ auth, path, component, loadComponent = true }) => {
|
||||||
|
|||||||
@ -3,6 +3,6 @@ import './styles/main.scss'
|
|||||||
import { h, hydrate } from 'preact'
|
import { h, hydrate } from 'preact'
|
||||||
import App from './components/app.tsx'
|
import App from './components/app.tsx'
|
||||||
|
|
||||||
const STATE = typeof __STATE__ !== 'undefined' ? __STATE__ : undefined
|
const state = typeof __STATE__ === 'undefined' ? { user: null } : __STATE__
|
||||||
|
|
||||||
hydrate(h(App, STATE), document.body)
|
hydrate(h(App, { state }), document.body)
|
||||||
|
|||||||
90
client/public/components/account_page.tsx
Normal file
90
client/public/components/account_page.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { h, type FunctionComponent } from 'preact'
|
||||||
|
import { useEffect, useRef } from 'preact/hooks'
|
||||||
|
import * as d3 from 'd3'
|
||||||
|
import rek from 'rek'
|
||||||
|
import Head from './head.ts'
|
||||||
|
import usePromise from '../../shared/hooks/use_promise.ts'
|
||||||
|
|
||||||
|
type AccountPageProps = {
|
||||||
|
number: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function empty(element: HTMLElement) {
|
||||||
|
while (element.lastElementChild) {
|
||||||
|
element.removeChild(element.lastElementChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccountPage: FunctionComponent<AccountPageProps> = ({ number }) => {
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null)
|
||||||
|
const sums = usePromise<{ month: string; totalAmount: number }[]>(() => rek(`/api/accounts/month-sum/${number}`))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
empty(graphRef.current!)
|
||||||
|
|
||||||
|
const width = 1000
|
||||||
|
const height = 1000
|
||||||
|
const margin = { left: 50, bottom: 40 }
|
||||||
|
const stageWidth = 1000 - margin.left
|
||||||
|
const stageHeight = 1000 - margin.bottom
|
||||||
|
|
||||||
|
const svg = d3
|
||||||
|
.select(graphRef.current)
|
||||||
|
.append('svg')
|
||||||
|
.attr('width', width)
|
||||||
|
.attr('height', height)
|
||||||
|
.append('g')
|
||||||
|
.attr('transform', `translate(${margin.left}, 0)`)
|
||||||
|
|
||||||
|
const x = d3.scaleBand(
|
||||||
|
sums.map(({ month }) => month),
|
||||||
|
[0, stageWidth],
|
||||||
|
)
|
||||||
|
|
||||||
|
svg.append('g').attr('transform', `translate(0, ${stageHeight})`).call(d3.axisBottom(x))
|
||||||
|
|
||||||
|
const y = d3.scaleLinear([0, d3.max(sums, (d) => Math.abs(d.totalAmount))], [stageHeight, 0])
|
||||||
|
|
||||||
|
svg.append('g').call(d3.axisLeft(y))
|
||||||
|
svg
|
||||||
|
.append('path')
|
||||||
|
.datum(sums)
|
||||||
|
.attr('fill', 'none')
|
||||||
|
.attr('stroke', 'steelblue')
|
||||||
|
.attr('stroke-width', 1.5)
|
||||||
|
.attr(
|
||||||
|
'd',
|
||||||
|
d3
|
||||||
|
.line()
|
||||||
|
.x((d) => x(d.month))
|
||||||
|
.y((d) => y(Math.abs(d.totalAmount))),
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<Head>
|
||||||
|
<title> : Konto</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<h1>Konto {number}</h1>
|
||||||
|
<div ref={graphRef} />
|
||||||
|
<table className='grid'>
|
||||||
|
<thead>
|
||||||
|
<th>Månad</th>
|
||||||
|
<th>Summa</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sums.map((sum) => (
|
||||||
|
<tr>
|
||||||
|
<td>{sum.month}</td>
|
||||||
|
<td>{sum.totalAmount}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccountPage
|
||||||
@ -23,7 +23,9 @@ const AccountsPage: FunctionComponent = () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{accounts.map((account) => (
|
{accounts.map((account) => (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{account.number}</td>
|
<td>
|
||||||
|
<a href={`/accounts/${account.number}`}>{account.number}</a>
|
||||||
|
</td>
|
||||||
<td>{account.description}</td>
|
<td>{account.description}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,19 +1,22 @@
|
|||||||
import { h, type FunctionComponent } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import { useCallback, useEffect, useRef } from 'preact/hooks'
|
import { useCallback, useEffect, useRef } from 'preact/hooks'
|
||||||
import { LocationProvider, Route, Router } from 'preact-iso'
|
import { LocationProvider, Router } from 'preact-iso'
|
||||||
import { get } from 'lowline'
|
import { get } from 'lowline'
|
||||||
|
import { AuthProvider } from '../../shared/contexts/auth.tsx'
|
||||||
|
import throttle from '../../shared/utils/throttle.ts'
|
||||||
import Head from './head.ts'
|
import Head from './head.ts'
|
||||||
import Footer from './footer.tsx'
|
import Footer from './footer.tsx'
|
||||||
import Header from './header.tsx'
|
import Header from './header.tsx'
|
||||||
import ErrorPage from './error_page.tsx'
|
import ErrorPage from './error_page.tsx'
|
||||||
|
import { AuthRoute, Refresh } from './route.tsx'
|
||||||
import routes from '../routes.ts'
|
import routes from '../routes.ts'
|
||||||
import throttle from '../../shared/utils/throttle.ts'
|
|
||||||
|
|
||||||
import s from './app.module.scss'
|
import s from './app.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
error?: Error
|
error?: Error
|
||||||
title?: string
|
title?: string
|
||||||
|
state: ANY
|
||||||
}
|
}
|
||||||
|
|
||||||
const scroll = () => {
|
const scroll = () => {
|
||||||
@ -22,7 +25,7 @@ const scroll = () => {
|
|||||||
window.scrollTo(0, offset || 0)
|
window.scrollTo(0, offset || 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const App: FunctionComponent<Props> = ({ error, title }) => {
|
const App: FunctionComponent<Props> = ({ error, title, state }) => {
|
||||||
const loadRef = useRef<boolean>(false)
|
const loadRef = useRef<boolean>(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -61,9 +64,10 @@ const App: FunctionComponent<Props> = ({ error, title }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LocationProvider>
|
<LocationProvider>
|
||||||
|
<AuthProvider user={state.user}>
|
||||||
<div id='app' className={s.base}>
|
<div id='app' className={s.base}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{title || 'Untitled'}</title>
|
<title>{title || 'BRF'}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Header routes={routes} />
|
<Header routes={routes} />
|
||||||
@ -74,14 +78,16 @@ const App: FunctionComponent<Props> = ({ error, title }) => {
|
|||||||
) : (
|
) : (
|
||||||
<Router onLoadStart={onLoadStart} onLoadEnd={onLoadEnd} onRouteChange={onRouteChange}>
|
<Router onLoadStart={onLoadStart} onLoadEnd={onLoadEnd} onRouteChange={onRouteChange}>
|
||||||
{routes.map((route) => (
|
{routes.map((route) => (
|
||||||
<Route key={route.path} path={route.path} component={route.component} />
|
<AuthRoute key={route.path} {...route} />
|
||||||
))}
|
))}
|
||||||
|
<Refresh path='/admin' />
|
||||||
</Router>
|
</Router>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
</AuthProvider>
|
||||||
</LocationProvider>
|
</LocationProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
5
client/public/components/current_user.module.scss
Normal file
5
client/public/components/current_user.module.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
38
client/public/components/current_user.tsx
Normal file
38
client/public/components/current_user.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { h, type FunctionComponent } from 'preact'
|
||||||
|
import { Show } from '@preact/signals/utils'
|
||||||
|
import { useComputed } from '@preact/signals'
|
||||||
|
import cn from 'classnames'
|
||||||
|
|
||||||
|
import { useAuth } from '../../shared/contexts/auth.tsx'
|
||||||
|
import s from './current_user.module.scss'
|
||||||
|
|
||||||
|
const CurrentUser: FunctionComponent<{ className?: string }> = ({ className }) => {
|
||||||
|
const { user } = useAuth()
|
||||||
|
const noUser = useComputed(() => !user.value)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(s.base, className)}>
|
||||||
|
<Show when={user}>
|
||||||
|
<div className={s.email}>{user.value?.email}</div>
|
||||||
|
|
||||||
|
<div className={s.links}>
|
||||||
|
<a href='/auth/logout' target='_parent'>
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={noUser}>
|
||||||
|
<div className={cn(s.base, className)}>
|
||||||
|
<p>You are not logged in</p>
|
||||||
|
|
||||||
|
<div className={s.links}>
|
||||||
|
<a href='/login'>Login</a>
|
||||||
|
<a href='/register'>Register</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CurrentUser
|
||||||
@ -1,6 +1,14 @@
|
|||||||
@use '../../shared/styles/utils';
|
@use '../../shared/styles/utils';
|
||||||
|
|
||||||
|
.base {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr max-content;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
|
grid-area: 2 / 1 / 3 / 2;
|
||||||
|
|
||||||
> ul {
|
> ul {
|
||||||
@include utils.wipe-list();
|
@include utils.wipe-list();
|
||||||
|
|
||||||
@ -11,6 +19,16 @@
|
|||||||
display: block;
|
display: block;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
> a {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.currentUser {
|
||||||
|
grid-area: 1 / 2 / 3 / 3;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { h, type FunctionComponent } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
|
import CurrentUser from './current_user.tsx'
|
||||||
import s from './header.module.scss'
|
import s from './header.module.scss'
|
||||||
|
|
||||||
import type { Route } from '../../../shared/types.ts'
|
import type { Route } from '../../../shared/types.ts'
|
||||||
|
|
||||||
const Header: FunctionComponent<{ routes: Route[] }> = ({ routes }) => (
|
const Header: FunctionComponent<{ routes: Route[] }> = ({ routes }) => (
|
||||||
<header>
|
<header className={s.base}>
|
||||||
<h1>BRF Tegeltrasten</h1>
|
<h1>BRF Tegeltrasten</h1>
|
||||||
<nav className={s.nav}>
|
<nav className={s.nav}>
|
||||||
<ul>
|
<ul>
|
||||||
@ -17,6 +18,7 @@ const Header: FunctionComponent<{ routes: Route[] }> = ({ routes }) => (
|
|||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
<CurrentUser className={s.currentUser} />
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
69
client/public/components/input.module.scss
Normal file
69
client/public/components/input.module.scss
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
@use 'sass:color';
|
||||||
|
|
||||||
|
.base {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&:not(:last-child):not(.noMargin) {
|
||||||
|
margin-bottom: var(--gutter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
order: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: var(--form-label-margin);
|
||||||
|
|
||||||
|
&:has(input:invalid) {
|
||||||
|
color: var(--form-invalid-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(input:valid) {
|
||||||
|
color: var(--form-valid-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.element {
|
||||||
|
order: 2;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid var(--form-border-color);
|
||||||
|
padding: var(--form-element-padding);
|
||||||
|
font-family: var(--body-font-family);
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-blue);
|
||||||
|
// box-shadow: 0 0 5px 0px color.scale(var(--color-blue), $alpha: -20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.touched &:invalid {
|
||||||
|
border-color: var(--form-invalid-color);
|
||||||
|
|
||||||
|
~ .label {
|
||||||
|
color: var(--form-invalid-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
// box-shadow: 0 0 5px 0px color.scale(var(--form-invalid-color), $alpha: -20%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.touched &:valid {
|
||||||
|
border-color: var(--form-valid-color);
|
||||||
|
|
||||||
|
~ .label {
|
||||||
|
color: var(--form-valid-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
// box-shadow: 0 0 5px 0px color.scale(var(--form-valid-color), $alpha: -20%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorLabel {
|
||||||
|
color: var(--form-invalid-color);
|
||||||
|
margin-top: var(--form-label-margin);
|
||||||
|
}
|
||||||
5
client/public/components/input.tsx
Normal file
5
client/public/components/input.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import inputFactory from '../../shared/components/input_factory.tsx'
|
||||||
|
|
||||||
|
import s from './input.module.scss'
|
||||||
|
|
||||||
|
export default inputFactory(s)
|
||||||
30
client/public/components/login_form.module.scss
Normal file
30
client/public/components/login_form.module.scss
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.root {
|
||||||
|
max-width: 400px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error,
|
||||||
|
.input {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
grid-column: 2 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
li:not(:last-child) {
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
77
client/public/components/login_form.tsx
Normal file
77
client/public/components/login_form.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { h, type FunctionComponent, type TargetedSubmitEvent } from 'preact'
|
||||||
|
import type { FetchError } from 'rek'
|
||||||
|
import { useCallback } from 'preact/hooks'
|
||||||
|
import { useAuth } from '../../shared/contexts/auth.tsx'
|
||||||
|
import rek from 'rek'
|
||||||
|
|
||||||
|
import useRequestState from '../../shared/hooks/use_request_state.ts'
|
||||||
|
import Input from './input.tsx'
|
||||||
|
import s from './login_form.module.scss'
|
||||||
|
|
||||||
|
const LoginForm: FunctionComponent = () => {
|
||||||
|
const [{ error, pending }, actions] = useRequestState<FetchError>()
|
||||||
|
const { user } = useAuth()
|
||||||
|
|
||||||
|
const login = useCallback(async (e: TargetedSubmitEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
actions.pending()
|
||||||
|
|
||||||
|
const form = e.currentTarget
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await rek.post(
|
||||||
|
'/auth/login',
|
||||||
|
{
|
||||||
|
email: form.email.value,
|
||||||
|
password: form.password.value,
|
||||||
|
},
|
||||||
|
{ response: false },
|
||||||
|
)
|
||||||
|
|
||||||
|
const lastVisit = response.headers.get('x-last-visit')
|
||||||
|
|
||||||
|
if (lastVisit) {
|
||||||
|
localStorage.setItem('lastVisit', lastVisit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
user.value = result
|
||||||
|
} catch (err) {
|
||||||
|
actions.error(err as FetchError)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={login} className={s.root}>
|
||||||
|
{error && <div className={s.error}>Error: {error.body?.message || error.message}</div>}
|
||||||
|
|
||||||
|
<Input className={s.input} name='email' label='Email' placeholder='Email' type='email' autoFocus required />
|
||||||
|
<Input
|
||||||
|
className={s.input}
|
||||||
|
type='password'
|
||||||
|
name='password'
|
||||||
|
label='Password'
|
||||||
|
placeholder='Password'
|
||||||
|
autoComplete='current-password'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button className={s.button} type='submit' disabled={pending}>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul className={s.links}>
|
||||||
|
<li>
|
||||||
|
<a href='/admin/forgot-password'>Forgot your password?</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='/admin/register'>No account? Register a new</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginForm
|
||||||
10
client/public/components/login_page.tsx
Normal file
10
client/public/components/login_page.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { h } from 'preact'
|
||||||
|
import LoginForm from './login_form.tsx'
|
||||||
|
|
||||||
|
const LoginPage = () => (
|
||||||
|
<section>
|
||||||
|
<LoginForm />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default LoginPage
|
||||||
34
client/public/components/route.tsx
Normal file
34
client/public/components/route.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { h, type FunctionComponent } from 'preact'
|
||||||
|
import { useLocation, type RouteProps } from 'preact-iso'
|
||||||
|
import { useAuth } from '../../shared/contexts/auth.tsx'
|
||||||
|
|
||||||
|
type AuthRouteProps = {
|
||||||
|
auth?: boolean
|
||||||
|
user?: any
|
||||||
|
} & RouteProps<any>
|
||||||
|
|
||||||
|
export const Refresh: FunctionComponent<{ path: string }> = () => {
|
||||||
|
location.replace(location.href)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Redirect: FunctionComponent<{ to: string; replace: boolean }> = ({ to, replace = false }) => {
|
||||||
|
const { route } = useLocation()
|
||||||
|
|
||||||
|
route(to, replace)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthRoute: FunctionComponent<AuthRouteProps> = ({ auth, component, ...props }) => {
|
||||||
|
const { user } = useAuth()
|
||||||
|
|
||||||
|
if (auth === true && !user.value) {
|
||||||
|
return <Redirect to='/login' replace={true} />
|
||||||
|
} else if (auth === false && user.value) {
|
||||||
|
const url = localStorage.getItem('lastVisit') || '/'
|
||||||
|
|
||||||
|
localStorage.removeItem('lastVisit')
|
||||||
|
|
||||||
|
return <Redirect to={url} replace={true} />
|
||||||
|
} else {
|
||||||
|
return h(component, props)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -71,7 +71,9 @@ const TransactionsPage: FunctionComponent = () => {
|
|||||||
{transactions.map((transaction) => (
|
{transactions.map((transaction) => (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{(transaction.transactionDate as unknown as string)?.slice(0, 10)}</td>
|
<td>{(transaction.transactionDate as unknown as string)?.slice(0, 10)}</td>
|
||||||
<td>{transaction.accountNumber}</td>
|
<td>
|
||||||
|
<a href={`/accounts/${transaction.accountNumber}`}>{transaction.accountNumber}</a>
|
||||||
|
</td>
|
||||||
<td className='tar'>
|
<td className='tar'>
|
||||||
{(transaction.amount as unknown as number) >= 0
|
{(transaction.amount as unknown as number) >= 0
|
||||||
? formatNumber(transaction.amount as unknown as number)
|
? formatNumber(transaction.amount as unknown as number)
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import Account from './components/account_page.tsx'
|
||||||
import Accounts from './components/accounts_page.tsx'
|
import Accounts from './components/accounts_page.tsx'
|
||||||
import Balances from './components/balances_page.tsx'
|
import Balances from './components/balances_page.tsx'
|
||||||
import Entries from './components/entries_page.tsx'
|
import Entries from './components/entries_page.tsx'
|
||||||
@ -5,6 +6,7 @@ import Entry from './components/entry_page.tsx'
|
|||||||
import Invoice from './components/invoice_page.tsx'
|
import Invoice from './components/invoice_page.tsx'
|
||||||
import Invoices from './components/invoices_page.tsx'
|
import Invoices from './components/invoices_page.tsx'
|
||||||
import InvoicesBySupplier from './components/invoices_by_supplier_page.tsx'
|
import InvoicesBySupplier from './components/invoices_by_supplier_page.tsx'
|
||||||
|
import Login from './components/login_page.tsx'
|
||||||
import Objects from './components/objects_page.tsx'
|
import Objects from './components/objects_page.tsx'
|
||||||
import Results from './components/results_page.tsx'
|
import Results from './components/results_page.tsx'
|
||||||
import Start from './components/start_page.tsx'
|
import Start from './components/start_page.tsx'
|
||||||
@ -16,18 +18,29 @@ export default [
|
|||||||
name: 'start',
|
name: 'start',
|
||||||
title: 'Start',
|
title: 'Start',
|
||||||
component: Start,
|
component: Start,
|
||||||
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/accounts',
|
path: '/accounts',
|
||||||
name: 'accounts',
|
name: 'accounts',
|
||||||
title: 'Konton',
|
title: 'Konton',
|
||||||
component: Accounts,
|
component: Accounts,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/accounts/:number',
|
||||||
|
name: 'account',
|
||||||
|
title: 'Konto',
|
||||||
|
component: Account,
|
||||||
|
nav: false,
|
||||||
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/entries',
|
path: '/entries',
|
||||||
name: 'entries',
|
name: 'entries',
|
||||||
title: 'Verifikat',
|
title: 'Verifikat',
|
||||||
component: Entries,
|
component: Entries,
|
||||||
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/entries/:id',
|
path: '/entries/:id',
|
||||||
@ -35,18 +48,21 @@ export default [
|
|||||||
title: 'Verifikat :id',
|
title: 'Verifikat :id',
|
||||||
component: Entry,
|
component: Entry,
|
||||||
nav: false,
|
nav: false,
|
||||||
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/objects',
|
path: '/objects',
|
||||||
name: 'objects',
|
name: 'objects',
|
||||||
title: 'Objekt',
|
title: 'Objekt',
|
||||||
component: Objects,
|
component: Objects,
|
||||||
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'invoices',
|
path: 'invoices',
|
||||||
name: 'invoices',
|
name: 'invoices',
|
||||||
title: 'Fakturor',
|
title: 'Fakturor',
|
||||||
component: Invoices,
|
component: Invoices,
|
||||||
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/invoices/:id',
|
path: '/invoices/:id',
|
||||||
@ -54,6 +70,7 @@ export default [
|
|||||||
title: 'Faktura :id',
|
title: 'Faktura :id',
|
||||||
component: Invoice,
|
component: Invoice,
|
||||||
nav: false,
|
nav: false,
|
||||||
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/invoices/by-supplier/:supplier',
|
path: '/invoices/by-supplier/:supplier',
|
||||||
@ -61,23 +78,35 @@ export default [
|
|||||||
title: 'Fakturor från leverantör',
|
title: 'Fakturor från leverantör',
|
||||||
component: InvoicesBySupplier,
|
component: InvoicesBySupplier,
|
||||||
nav: false,
|
nav: false,
|
||||||
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/balances',
|
path: '/balances',
|
||||||
name: 'balances',
|
name: 'balances',
|
||||||
title: 'Balanser',
|
title: 'Balanser',
|
||||||
component: Balances,
|
component: Balances,
|
||||||
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/results',
|
path: '/results',
|
||||||
name: 'results',
|
name: 'results',
|
||||||
title: 'Resultat',
|
title: 'Resultat',
|
||||||
component: Results,
|
component: Results,
|
||||||
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/transactions',
|
path: '/transactions',
|
||||||
name: 'transactions',
|
name: 'transactions',
|
||||||
title: 'Transaktioner',
|
title: 'Transaktioner',
|
||||||
component: Transactions,
|
component: Transactions,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
title: 'Logga in',
|
||||||
|
component: Login,
|
||||||
|
nav: false,
|
||||||
|
auth: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,3 +1,14 @@
|
|||||||
|
:root {
|
||||||
|
--color-green: green;
|
||||||
|
--color-red: red;
|
||||||
|
--gutter: 16px;
|
||||||
|
--form-valid-color: var(--color-green);
|
||||||
|
--form-invalid-color: var(--color-red);
|
||||||
|
--form-border-color: #d2d6de;
|
||||||
|
--form-element-padding: 9px;
|
||||||
|
--form-label-margin: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*:before,
|
*:before,
|
||||||
*:after {
|
*:after {
|
||||||
|
|||||||
15
client/shared/contexts/auth.tsx
Normal file
15
client/shared/contexts/auth.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { h, createContext, type FunctionComponent } from 'preact'
|
||||||
|
import { useSignal } from '@preact/signals'
|
||||||
|
import { useContext } from 'preact/hooks'
|
||||||
|
|
||||||
|
type AuthContextType = { user: ANY }
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType | null>(null)
|
||||||
|
|
||||||
|
export const AuthProvider: FunctionComponent<{ user: ANY }> = ({ children, user }) => {
|
||||||
|
const userSignal = useSignal(user)
|
||||||
|
|
||||||
|
return <AuthContext.Provider value={{ user: userSignal }}>{children}</AuthContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuth = () => useContext(AuthContext) as AuthContextType
|
||||||
@ -2,7 +2,7 @@
|
|||||||
-- PostgreSQL database dump
|
-- PostgreSQL database dump
|
||||||
--
|
--
|
||||||
|
|
||||||
\restrict kNYhdwOhwE9I3bgAzdljyYgB5xyEpjhiaSCeYZfp84v3ey1GpvsdxX4U8Y8fQM3
|
\restrict wU1dfnHqRTt2y70XvbaA1LDda0PgjnckSPP98WqYNl91ofZctLJ9wjIOEE8bpwR
|
||||||
|
|
||||||
-- 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
|
||||||
@ -71,10 +71,10 @@ ALTER SEQUENCE public.account_id_seq OWNED BY public.account.id;
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: aliasesToSupplier; Type: TABLE; Schema: public; Owner: -
|
-- Name: aliases_supplier; Type: TABLE; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE public."aliasesToSupplier" (
|
CREATE TABLE public.aliases_supplier (
|
||||||
id integer NOT NULL,
|
id integer NOT NULL,
|
||||||
"supplierId" integer NOT NULL,
|
"supplierId" integer NOT NULL,
|
||||||
alias text NOT NULL
|
alias text NOT NULL
|
||||||
@ -82,10 +82,10 @@ CREATE TABLE public."aliasesToSupplier" (
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: aliasesToSupplier_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
-- Name: aliases_supplier_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE SEQUENCE public."aliasesToSupplier_id_seq"
|
CREATE SEQUENCE public.aliases_supplier_id_seq
|
||||||
AS integer
|
AS integer
|
||||||
START WITH 1
|
START WITH 1
|
||||||
INCREMENT BY 1
|
INCREMENT BY 1
|
||||||
@ -95,10 +95,10 @@ CREATE SEQUENCE public."aliasesToSupplier_id_seq"
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: aliasesToSupplier_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
-- Name: aliases_supplier_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER SEQUENCE public."aliasesToSupplier_id_seq" OWNED BY public."aliasesToSupplier".id;
|
ALTER SEQUENCE public.aliases_supplier_id_seq OWNED BY public.aliases_supplier.id;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@ -199,10 +199,10 @@ ALTER SEQUENCE public.file_id_seq OWNED BY public.file.id;
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: filesToInvoice; Type: TABLE; Schema: public; Owner: -
|
-- Name: files_invoice; Type: TABLE; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE public."filesToInvoice" (
|
CREATE TABLE public.files_invoice (
|
||||||
"invoiceId" integer NOT NULL,
|
"invoiceId" integer NOT NULL,
|
||||||
"fileId" integer NOT NULL
|
"fileId" integer NOT NULL
|
||||||
);
|
);
|
||||||
@ -442,10 +442,10 @@ ALTER SEQUENCE public.transaction_id_seq OWNED BY public.transaction.id;
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: transactionsToObjects; Type: TABLE; Schema: public; Owner: -
|
-- Name: transactions_objects; Type: TABLE; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE public."transactionsToObjects" (
|
CREATE TABLE public.transactions_objects (
|
||||||
"transactionId" integer NOT NULL,
|
"transactionId" integer NOT NULL,
|
||||||
"objectId" integer NOT NULL
|
"objectId" integer NOT NULL
|
||||||
);
|
);
|
||||||
@ -459,10 +459,10 @@ ALTER TABLE ONLY public.account ALTER COLUMN id SET DEFAULT nextval('public.acco
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: aliasesToSupplier id; Type: DEFAULT; Schema: public; Owner: -
|
-- Name: aliases_supplier id; Type: DEFAULT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public."aliasesToSupplier" ALTER COLUMN id SET DEFAULT nextval('public."aliasesToSupplier_id_seq"'::regclass);
|
ALTER TABLE ONLY public.aliases_supplier ALTER COLUMN id SET DEFAULT nextval('public.aliases_supplier_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@ -560,19 +560,19 @@ ALTER TABLE ONLY public.account
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: aliasesToSupplier aliasesToSupplier_alias_key; Type: CONSTRAINT; Schema: public; Owner: -
|
-- Name: aliases_supplier aliases_supplier_alias_key; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public."aliasesToSupplier"
|
ALTER TABLE ONLY public.aliases_supplier
|
||||||
ADD CONSTRAINT "aliasesToSupplier_alias_key" UNIQUE (alias);
|
ADD CONSTRAINT aliases_supplier_alias_key UNIQUE (alias);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: aliasesToSupplier aliasesToSupplier_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
-- Name: aliases_supplier aliases_supplier_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public."aliasesToSupplier"
|
ALTER TABLE ONLY public.aliases_supplier
|
||||||
ADD CONSTRAINT "aliasesToSupplier_pkey" PRIMARY KEY (id);
|
ADD CONSTRAINT aliases_supplier_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@ -600,11 +600,11 @@ ALTER TABLE ONLY public.file
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: filesToInvoice filesToInvoice_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
-- Name: files_invoice files_invoice_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public."filesToInvoice"
|
ALTER TABLE ONLY public.files_invoice
|
||||||
ADD CONSTRAINT "filesToInvoice_pkey" PRIMARY KEY ("invoiceId", "fileId");
|
ADD CONSTRAINT files_invoice_pkey PRIMARY KEY ("invoiceId", "fileId");
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@ -631,6 +631,22 @@ ALTER TABLE ONLY public."financialYear"
|
|||||||
ADD CONSTRAINT "financialYear_year_key" UNIQUE (year);
|
ADD CONSTRAINT "financialYear_year_key" UNIQUE (year);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: invoice invoice_fiskenNumber_key; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.invoice
|
||||||
|
ADD CONSTRAINT "invoice_fiskenNumber_key" UNIQUE ("fiskenNumber");
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: invoice invoice_phmNumber_key; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.invoice
|
||||||
|
ADD CONSTRAINT "invoice_phmNumber_key" UNIQUE ("phmNumber");
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: invoice invoice_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
-- Name: invoice invoice_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@ -696,35 +712,35 @@ ALTER TABLE ONLY public.transaction
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: transactionsToObjects transactionsToObjects_transactionId_objectId_key; Type: CONSTRAINT; Schema: public; Owner: -
|
-- Name: transactions_objects transactions_objects_transactionId_objectId_key; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public."transactionsToObjects"
|
ALTER TABLE ONLY public.transactions_objects
|
||||||
ADD CONSTRAINT "transactionsToObjects_transactionId_objectId_key" UNIQUE ("transactionId", "objectId");
|
ADD CONSTRAINT "transactions_objects_transactionId_objectId_key" UNIQUE ("transactionId", "objectId");
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: aliasesToSupplier aliasesToSupplier_supplierId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
-- Name: aliases_supplier aliases_supplier_supplierId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public."aliasesToSupplier"
|
ALTER TABLE ONLY public.aliases_supplier
|
||||||
ADD CONSTRAINT "aliasesToSupplier_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES public.supplier(id);
|
ADD CONSTRAINT "aliases_supplier_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES public.supplier(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: filesToInvoice filesToInvoice_fileId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
-- Name: files_invoice files_invoice_fileId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public."filesToInvoice"
|
ALTER TABLE ONLY public.files_invoice
|
||||||
ADD CONSTRAINT "filesToInvoice_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES public.file(id);
|
ADD CONSTRAINT "files_invoice_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES public.file(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: filesToInvoice filesToInvoice_invoiceId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
-- Name: files_invoice files_invoice_invoiceId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public."filesToInvoice"
|
ALTER TABLE ONLY public.files_invoice
|
||||||
ADD CONSTRAINT "filesToInvoice_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES public.invoice(id);
|
ADD CONSTRAINT "files_invoice_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES public.invoice(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@ -784,24 +800,24 @@ ALTER TABLE ONLY public.transaction
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: transactionsToObjects transactionsToObjects_objectId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
-- Name: transactions_objects transactions_objects_objectId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public."transactionsToObjects"
|
ALTER TABLE ONLY public.transactions_objects
|
||||||
ADD CONSTRAINT "transactionsToObjects_objectId_fkey" FOREIGN KEY ("objectId") REFERENCES public.object(id);
|
ADD CONSTRAINT "transactions_objects_objectId_fkey" FOREIGN KEY ("objectId") REFERENCES public.object(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: transactionsToObjects transactionsToObjects_transactionId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
-- Name: transactions_objects transactions_objects_transactionId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public."transactionsToObjects"
|
ALTER TABLE ONLY public.transactions_objects
|
||||||
ADD CONSTRAINT "transactionsToObjects_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES public.transaction(id);
|
ADD CONSTRAINT "transactions_objects_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES public.transaction(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|
||||||
\unrestrict kNYhdwOhwE9I3bgAzdljyYgB5xyEpjhiaSCeYZfp84v3ey1GpvsdxX4U8Y8fQM3
|
\unrestrict wU1dfnHqRTt2y70XvbaA1LDda0PgjnckSPP98WqYNl91ofZctLJ9wjIOEE8bpwR
|
||||||
|
|
||||||
|
|||||||
@ -20,11 +20,11 @@ auth_tables = [
|
|||||||
accounting_tables = [
|
accounting_tables = [
|
||||||
'account',
|
'account',
|
||||||
'accountBalance',
|
'accountBalance',
|
||||||
'aliasesToSupplier',
|
'aliases_supplier',
|
||||||
'dimension',
|
'dimension',
|
||||||
'entry',
|
'entry',
|
||||||
'file',
|
'file',
|
||||||
'filesToInvoice',
|
'files_invoice',
|
||||||
'financialYear',
|
'financialYear',
|
||||||
'invoice',
|
'invoice',
|
||||||
'journal',
|
'journal',
|
||||||
@ -32,7 +32,7 @@ accounting_tables = [
|
|||||||
'supplier',
|
'supplier',
|
||||||
'supplierType',
|
'supplierType',
|
||||||
'transaction',
|
'transaction',
|
||||||
'transactionsToObjects',
|
'transactions_objects',
|
||||||
]
|
]
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
|||||||
54
package.json
54
package.json
@ -12,7 +12,8 @@
|
|||||||
"build:client": "VITE_ENTRY=public vite build && VITE_ENTRY=admin vite build",
|
"build:client": "VITE_ENTRY=public vite build && VITE_ENTRY=admin vite build",
|
||||||
"build:server": "vite build --ssr",
|
"build:server": "vite build --ssr",
|
||||||
"clean": "rm -r dist",
|
"clean": "rm -r dist",
|
||||||
"format": "prettier --write .",
|
"fmt": "oxfmt",
|
||||||
|
"fmt:check": "oxfmt --check",
|
||||||
"lint": "oxlint",
|
"lint": "oxlint",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"start": "node server/index.ts",
|
"start": "node server/index.ts",
|
||||||
@ -28,49 +29,48 @@
|
|||||||
"@bmp/highlight-stack": "^0.1.2",
|
"@bmp/highlight-stack": "^0.1.2",
|
||||||
"@domp/suppress": "^0.4.0",
|
"@domp/suppress": "^0.4.0",
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/middie": "^9.0.3",
|
"@fastify/middie": "^9.3.2",
|
||||||
"@fastify/session": "^11.1.1",
|
"@fastify/session": "^11.1.1",
|
||||||
"@fastify/static": "^8.3.0",
|
"@fastify/static": "^9.1.3",
|
||||||
"@fastify/type-provider-typebox": "^6.1.0",
|
"@fastify/type-provider-typebox": "^6.1.0",
|
||||||
|
"@preact/signals": "^2.9.1",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
"d3": "^7.9.0",
|
||||||
"easy-tz": "^0.2.0",
|
"easy-tz": "^0.2.0",
|
||||||
"fastify": "^5.6.2",
|
"fastify": "^5.8.5",
|
||||||
"fastify-plugin": "^5.1.0",
|
"fastify-plugin": "^6.0.0",
|
||||||
"fastify-session-redis-store": "^7.1.2",
|
"fastify-session-redis-store": "^7.1.2",
|
||||||
"fastify-type-provider-zod": "^6.1.0",
|
"fastify-type-provider-zod": "^6.1.0",
|
||||||
"ioredis": "^5.8.2",
|
"ioredis": "^5.11.1",
|
||||||
"kysely": "^0.28.9",
|
"kysely": "^0.29.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.18.1",
|
||||||
"lowline": "^0.4.2",
|
"lowline": "^0.4.2",
|
||||||
"mini-qs": "^0.2.0",
|
"mini-qs": "^0.2.0",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.21.0",
|
||||||
"pg-protocol": "^1.10.3",
|
"pg-protocol": "^1.14.0",
|
||||||
"pino-abstract-transport": "^3.0.0",
|
"pino-abstract-transport": "^3.0.0",
|
||||||
"preact": "^10.28.0",
|
"preact": "^10.29.2",
|
||||||
"preact-iso": "^2.11.0",
|
"preact-iso": "^2.12.0",
|
||||||
"preact-router": "^4.1.2",
|
"preact-router": "^4.1.2",
|
||||||
"rek": "^0.8.1",
|
"rek": "^0.8.1",
|
||||||
"zod": "^4.2.1"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.10",
|
"@preact/preset-vite": "^2.10.5",
|
||||||
"@preact/preset-vite": "^2.10.1",
|
|
||||||
"@testing-library/preact": "^3.2.4",
|
"@testing-library/preact": "^3.2.4",
|
||||||
"@types/d3-dsv": "^3.0.7",
|
"@types/d3-dsv": "^3.0.7",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.24",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^25.9.3",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251126.1",
|
"@typescript/native-preview": "7.0.0-dev.20260614.1",
|
||||||
"d3-dsv": "^3.0.1",
|
"d3-dsv": "^3.0.1",
|
||||||
"esbuild": "^0.27.0",
|
|
||||||
"globals": "^16.0.0",
|
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^27.2.0",
|
"jsdom": "^29.1.1",
|
||||||
"lint-staged": "^16.2.7",
|
"lint-staged": "^17.0.7",
|
||||||
"oxlint": "^1.29.0",
|
"oxfmt": "^0.54.0",
|
||||||
"prettier": "^3.5.3",
|
"oxlint": "^1.69.0",
|
||||||
"sass": "^1.85.1",
|
"sass": "^1.101.0",
|
||||||
"typebox": "^1.0.55",
|
"typebox": "^1.2.10",
|
||||||
"vite": "^7.2.4"
|
"vite": "^8.0.16"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3894
pnpm-lock.yaml
generated
3894
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -26,9 +26,9 @@ const maxLoginAttempts = 5
|
|||||||
const requireVerification = true
|
const requireVerification = true
|
||||||
|
|
||||||
const redirects = {
|
const redirects = {
|
||||||
login: '/admin/',
|
login: '/',
|
||||||
logout: '/admin/login',
|
logout: '/login',
|
||||||
register: '/admin/login',
|
register: '/login',
|
||||||
}
|
}
|
||||||
|
|
||||||
const remember = {
|
const remember = {
|
||||||
|
|||||||
@ -321,7 +321,7 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
|
|||||||
}
|
}
|
||||||
|
|
||||||
await trx
|
await trx
|
||||||
.insertInto('transactionsToObjects')
|
.insertInto('transactions_objects')
|
||||||
.values({
|
.values({
|
||||||
transactionId,
|
transactionId,
|
||||||
objectId,
|
objectId,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import build from 'pino-abstract-transport'
|
import build from 'pino-abstract-transport'
|
||||||
|
|
||||||
export default function dbTransport(create) {
|
export default function dbTransport(create: ANY) {
|
||||||
return build(async (source) => {
|
return build(async (source) => {
|
||||||
for await (const obj of source) {
|
for await (const obj of source) {
|
||||||
// TODO decide how to handle lower log levels
|
// TODO decide how to handle lower log levels
|
||||||
|
|||||||
@ -74,6 +74,13 @@ const login: RouteHandler<{ Body: z.infer<typeof BodySchema> }> = async function
|
|||||||
.where('id', '=', user.id)
|
.where('id', '=', user.id)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
|
const lastVisit = request.session.get('lastVisit')
|
||||||
|
|
||||||
|
if (lastVisit) {
|
||||||
|
reply.header('x-last-visit', lastVisit)
|
||||||
|
request.session.set('lastVisit', undefined)
|
||||||
|
}
|
||||||
|
|
||||||
return reply.status(200).send(_.omit(user, 'password'))
|
return reply.status(200).send(_.omit(user, 'password'))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.log.error(err)
|
this.log.error(err)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const schema: FastifySchema = {
|
|||||||
const logout: RouteHandler = async function (request, reply) {
|
const logout: RouteHandler = async function (request, reply) {
|
||||||
await request.logout()
|
await request.logout()
|
||||||
|
|
||||||
return reply.redirect('/admin/login')
|
return reply.redirect('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import * as z from 'zod'
|
|||||||
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
|
|
||||||
import { AccountSchema } from '../../schemas/db.ts'
|
import { AccountSchema } from '../../schemas/db.ts'
|
||||||
|
import { sql } from 'kysely'
|
||||||
|
|
||||||
const accountRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
const accountRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||||
const { db } = fastify
|
const { db } = fastify
|
||||||
@ -20,6 +21,88 @@ const accountRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fastify.route({
|
||||||
|
url: '/:number',
|
||||||
|
method: 'GET',
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
number: z.coerce.number(),
|
||||||
|
}),
|
||||||
|
// response: {
|
||||||
|
// 200: z.array(AccountSchema),
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
handler(request) {
|
||||||
|
return db
|
||||||
|
.selectFrom('account as a')
|
||||||
|
.innerJoin('transaction as t', 't.accountNumber', 'a.number')
|
||||||
|
.innerJoin('entry as e', 'e.id', 't.entryId')
|
||||||
|
.selectAll('a')
|
||||||
|
.select('e.transactionDate')
|
||||||
|
.where('a.number', '=', request.params.number)
|
||||||
|
.orderBy('e.transactionDate', 'asc')
|
||||||
|
.execute()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
fastify.route({
|
||||||
|
url: '/month-sum/:number',
|
||||||
|
method: 'GET',
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
number: z.coerce.number(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
response: {
|
||||||
|
200: z.array(
|
||||||
|
z.object({
|
||||||
|
month: z.string(),
|
||||||
|
totalAmount: z.coerce.number(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler(request) {
|
||||||
|
return db
|
||||||
|
.selectFrom('transaction as t')
|
||||||
|
.innerJoin('entry as e', 'e.id', 't.entryId')
|
||||||
|
.select([
|
||||||
|
sql<string>`to_char(date_trunc('month', e."transactionDate"), 'YYYY-MM')`.as('month'),
|
||||||
|
sql<number>`sum(amount)`.as('totalAmount'),
|
||||||
|
])
|
||||||
|
.groupBy(sql`date_trunc('month', e."transactionDate")`)
|
||||||
|
.orderBy('month')
|
||||||
|
.where('t.accountNumber', '=', request.params.number)
|
||||||
|
.execute()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
fastify.route({
|
||||||
|
url: '/year-sum/:number',
|
||||||
|
method: 'GET',
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
number: z.coerce.number(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
handler(request) {
|
||||||
|
return (
|
||||||
|
db
|
||||||
|
.selectFrom('transaction as t')
|
||||||
|
.innerJoin('entry as e', 'e.id', 't.entryId')
|
||||||
|
// .select([sql`date_trunc('year', e."transactionDate")`.as('year'), sql<number>`sum(amount)`.as('totalAmount')])
|
||||||
|
.select([
|
||||||
|
sql`to_char(date_trunc('year', e."transactionDate"), 'YYYY')`.as('year'),
|
||||||
|
sql<number>`sum(amount)`.as('totalAmount'),
|
||||||
|
])
|
||||||
|
.groupBy(sql`date_trunc('year', e."transactionDate")`)
|
||||||
|
.orderBy('year')
|
||||||
|
.where('t.accountNumber', '=', request.params.number)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const invoiceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
|||||||
jsonArrayFrom(
|
jsonArrayFrom(
|
||||||
eb
|
eb
|
||||||
.selectFrom('file as f')
|
.selectFrom('file as f')
|
||||||
.innerJoin('filesToInvoice as fi', 'f.id', 'fi.fileId')
|
.innerJoin('files_invoice as fi', 'f.id', 'fi.fileId')
|
||||||
.select(['id', 'filename'])
|
.select(['id', 'filename'])
|
||||||
.whereRef('fi.invoiceId', '=', 'i.id'),
|
.whereRef('fi.invoiceId', '=', 'i.id'),
|
||||||
).as('files'),
|
).as('files'),
|
||||||
@ -89,7 +89,7 @@ const invoiceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
|||||||
jsonArrayFrom(
|
jsonArrayFrom(
|
||||||
eb
|
eb
|
||||||
.selectFrom('file as f')
|
.selectFrom('file as f')
|
||||||
.innerJoin('filesToInvoice as fi', 'f.id', 'fi.fileId')
|
.innerJoin('files_invoice as fi', 'f.id', 'fi.fileId')
|
||||||
.select(['id', 'filename'])
|
.select(['id', 'filename'])
|
||||||
.whereRef('fi.invoiceId', '=', 'i.id'),
|
.whereRef('fi.invoiceId', '=', 'i.id'),
|
||||||
).as('files'),
|
).as('files'),
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const objectRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
|||||||
async handler(request) {
|
async handler(request) {
|
||||||
return db
|
return db
|
||||||
.selectFrom('transaction as t')
|
.selectFrom('transaction as t')
|
||||||
.innerJoin('transactionsToObjects as to', 't.id', 'to.transactionId')
|
.innerJoin('transactions_objects as to', 't.id', 'to.transactionId')
|
||||||
.innerJoin('entry as e', 'e.id', 't.entryId')
|
.innerJoin('entry as e', 'e.id', 't.entryId')
|
||||||
.select(['t.entryId', 'e.transactionDate', 't.accountNumber', 't.amount'])
|
.select(['t.entryId', 'e.transactionDate', 't.accountNumber', 't.amount'])
|
||||||
.where('to.objectId', '=', request.params.id)
|
.where('to.objectId', '=', request.params.id)
|
||||||
|
|||||||
@ -48,6 +48,19 @@ export default async (options?: FastifyServerOptions) => {
|
|||||||
server.register(vitePlugin, {
|
server.register(vitePlugin, {
|
||||||
mode: env.NODE_ENV,
|
mode: env.NODE_ENV,
|
||||||
createErrorHandler: ErrorHandler,
|
createErrorHandler: ErrorHandler,
|
||||||
|
createPreHandler(route) {
|
||||||
|
return async function preHandler(request, reply) {
|
||||||
|
if (request.session.userId) {
|
||||||
|
return (reply.ctx = { user: await request.user })
|
||||||
|
} else if (route.auth) {
|
||||||
|
if (request.url !== '/') {
|
||||||
|
request.session.set('lastVisit', request.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.redirect('/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
entries: {
|
entries: {
|
||||||
public: {
|
public: {
|
||||||
path: '/',
|
path: '/',
|
||||||
@ -56,15 +69,6 @@ export default async (options?: FastifyServerOptions) => {
|
|||||||
admin: {
|
admin: {
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
template: templateAdmin,
|
template: templateAdmin,
|
||||||
createPreHandler(route) {
|
|
||||||
return async function preHandler(request, reply) {
|
|
||||||
if (request.session.userId) {
|
|
||||||
return (reply.ctx = { user: await request.user })
|
|
||||||
} else if (route.auth) {
|
|
||||||
return reply.redirect('/admin/login')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -16,7 +16,7 @@ interface Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default ({ content, css, head, preload, script, state }: Options) => html`<!DOCTYPE html>
|
export default ({ content, css, head, preload, script, state }: Options) => html`<!DOCTYPE html>
|
||||||
<html lang='sv-SE'>
|
<html lang="sv-SE">
|
||||||
<head>
|
<head>
|
||||||
<link rel="icon" href="/favicon.svg" />
|
<link rel="icon" href="/favicon.svg" />
|
||||||
<script type="module" src="${script}"></script>
|
<script type="module" src="${script}"></script>
|
||||||
@ -38,6 +38,9 @@ export default ({ content, css, head, preload, script, state }: Options) => html
|
|||||||
<body>
|
<body>
|
||||||
${content}
|
${content}
|
||||||
${state?.then((state) => `<script>window.__STATE__ = ${JSON.stringify(state)}</script>`)}
|
${state?.then((state) => `<script>window.__STATE__ = ${JSON.stringify(state)}</script>`)}
|
||||||
<script>var offset=window.history.state&&window.history.state.scrollTop||0;if(offset)window.scrollTo(0,offset)</script>
|
<script>
|
||||||
|
var offset = (window.history.state && window.history.state.scrollTop) || 0;
|
||||||
|
if (offset) window.scrollTo(0, offset);
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|||||||
1
shared/global.d.ts
vendored
1
shared/global.d.ts
vendored
@ -13,6 +13,7 @@ declare global {
|
|||||||
|
|
||||||
declare module 'fastify' {
|
declare module 'fastify' {
|
||||||
interface Session {
|
interface Session {
|
||||||
|
lastVisit?: string
|
||||||
userId: number
|
userId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export interface AdmissionsRoles {
|
|||||||
roleId: number
|
roleId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AliasesToSupplier {
|
export interface AliasesSupplier {
|
||||||
alias: string
|
alias: string
|
||||||
id: Generated<number>
|
id: Generated<number>
|
||||||
supplierId: number
|
supplierId: number
|
||||||
@ -108,7 +108,7 @@ export interface File {
|
|||||||
id: Generated<number>
|
id: Generated<number>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FilesToInvoice {
|
export interface FilesInvoice {
|
||||||
fileId: number
|
fileId: number
|
||||||
invoiceId: number
|
invoiceId: number
|
||||||
}
|
}
|
||||||
@ -206,7 +206,7 @@ export interface Transaction {
|
|||||||
transactionDate: Timestamp | null
|
transactionDate: Timestamp | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionsToObjects {
|
export interface TransactionsObjects {
|
||||||
objectId: number
|
objectId: number
|
||||||
transactionId: number
|
transactionId: number
|
||||||
}
|
}
|
||||||
@ -237,13 +237,13 @@ export interface DB {
|
|||||||
accountBalance: AccountBalance
|
accountBalance: AccountBalance
|
||||||
admission: Admission
|
admission: Admission
|
||||||
admissions_roles: AdmissionsRoles
|
admissions_roles: AdmissionsRoles
|
||||||
aliasesToSupplier: AliasesToSupplier
|
aliases_Supplier: AliasesSupplier
|
||||||
dimension: Dimension
|
dimension: Dimension
|
||||||
emailToken: EmailToken
|
emailToken: EmailToken
|
||||||
entry: Entry
|
entry: Entry
|
||||||
error: Error
|
error: Error
|
||||||
file: File
|
file: File
|
||||||
filesToInvoice: FilesToInvoice
|
files_invoice: FilesInvoice
|
||||||
financialYear: FinancialYear
|
financialYear: FinancialYear
|
||||||
invite: Invite
|
invite: Invite
|
||||||
invites_roles: InvitesRoles
|
invites_roles: InvitesRoles
|
||||||
@ -255,7 +255,7 @@ export interface DB {
|
|||||||
supplier: Supplier
|
supplier: Supplier
|
||||||
supplierType: SupplierType
|
supplierType: SupplierType
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
transactionsToObjects: TransactionsToObjects
|
transactions_objects: TransactionsObjects
|
||||||
user: User
|
user: User
|
||||||
users_roles: UsersRoles
|
users_roles: UsersRoles
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user