Compare commits

..

No commits in common. "dev" and "master" have entirely different histories.
dev ... master

67 changed files with 2127 additions and 2769 deletions

View File

@ -1,20 +0,0 @@
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
}

View File

@ -1,20 +0,0 @@
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
}

View File

@ -1,20 +0,0 @@
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
}

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/balances name: /api/balances
type: http type: http
seq: 4 seq: 3
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/entries/:id name: /api/entries/:id
type: http type: http
seq: 6 seq: 5
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/entries name: /api/entries
type: http type: http
seq: 5 seq: 4
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/errors name: /api/errors
type: http type: http
seq: 23 seq: 22
} }
delete { delete {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/financial-years name: /api/financial-years
type: http type: http
seq: 7 seq: 6
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/invites name: /api/invites
type: http type: http
seq: 11 seq: 10
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/invoices/:id name: /api/invoices/:id
type: http type: http
seq: 9 seq: 8
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/invoices/total-amount name: /api/invoices/total-amount
type: http type: http
seq: 10 seq: 9
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/invoices name: /api/invoices
type: http type: http
seq: 8 seq: 7
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/journals name: /api/journals
type: http type: http
seq: 22 seq: 21
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/objects/:id/transactions name: /api/objects/:id/transactions
type: http type: http
seq: 13 seq: 12
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/objects name: /api/objects
type: http type: http
seq: 12 seq: 11
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/results/:year name: /api/results/:year
type: http type: http
seq: 15 seq: 14
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/results name: /api/results
type: http type: http
seq: 14 seq: 13
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/roles name: /api/roles
type: http type: http
seq: 16 seq: 15
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/suppliers/:id name: /api/suppliers/:id
type: http type: http
seq: 18 seq: 17
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/suppliers/merge name: /api/suppliers/merge
type: http type: http
seq: 19 seq: 18
} }
post { post {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/suppliers name: /api/suppliers
type: http type: http
seq: 17 seq: 16
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/transactions name: /api/transactions
type: http type: http
seq: 20 seq: 19
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/users name: /api/users
type: http type: http
seq: 21 seq: 20
} }
get { get {

View File

@ -1,9 +1,9 @@
{ {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [ "*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
"oxlint --fix", "oxlint --fix",
"oxfmt" "prettier --write"
], ],
"*.{css,scss,json}": [ "*.{css,scss,json}": [
"oxfmt" "prettier --write"
] ]
} }

18
.prettierignore Normal file
View File

@ -0,0 +1,18 @@
dist
dump
*.*
!*.cjs
!*.css
!*.cts
!*.html
!*.js
!*.json
!*.jsx
!*.mjs
!*.mts
!*.scss
!*.ts
!*.tsx
!*.yaml
!*.yml
pnpm-lock.yaml

17
.prettierrc Normal file
View File

@ -0,0 +1,17 @@
{
"jsxSingleQuote": true,
"printWidth": 120,
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"embeddedLanguageFormatting": "off",
"overrides": [
{
"files": "*.scss",
"options": {
"trailingComma": "none"
}
}
]
}

View File

@ -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('files_invoice').values({ fileId: file.id, invoiceId: invoice.id }).execute() await trx.insertInto('filesToInvoice').values({ fileId: file.id, invoiceId: invoice.id }).execute()
} }
await trx.commit().execute() await trx.commit().execute()

View File

@ -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('files_invoice').values({ fileId: file.id, invoiceId: invoice.id }).execute() await trx.insertInto('filesToInvoice').values({ fileId: file.id, invoiceId: invoice.id }).execute()
} }
} }

View File

@ -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 '../../shared/contexts/current_user.ts' import CurrentUserContext from '../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}>
BRF Admin Carson Admin
</a> </a>
<header className={s.header}> <header className={s.header}>

View File

@ -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 '../../shared/contexts/current_user.ts' import { useCurrentUser } from '../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 }) => {

View File

@ -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 '../../shared/contexts/current_user.ts' import { useCurrentUser } from '../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'

View File

@ -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 '../../shared/contexts/current_user.ts' import { useCurrentUser } from '../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 }) => {

View File

@ -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' ? { user: null } : __STATE__ const STATE = typeof __STATE__ !== 'undefined' ? __STATE__ : undefined
hydrate(h(App, { state }), document.body) hydrate(h(App, STATE), document.body)

View File

@ -1,90 +0,0 @@
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

View File

@ -23,9 +23,7 @@ const AccountsPage: FunctionComponent = () => {
<tbody> <tbody>
{accounts.map((account) => ( {accounts.map((account) => (
<tr> <tr>
<td> <td>{account.number}</td>
<a href={`/accounts/${account.number}`}>{account.number}</a>
</td>
<td>{account.description}</td> <td>{account.description}</td>
</tr> </tr>
))} ))}

View File

@ -1,22 +1,19 @@
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, Router } from 'preact-iso' import { LocationProvider, Route, 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 = () => {
@ -25,7 +22,7 @@ const scroll = () => {
window.scrollTo(0, offset || 0) window.scrollTo(0, offset || 0)
} }
const App: FunctionComponent<Props> = ({ error, title, state }) => { const App: FunctionComponent<Props> = ({ error, title }) => {
const loadRef = useRef<boolean>(false) const loadRef = useRef<boolean>(false)
useEffect(() => { useEffect(() => {
@ -64,10 +61,9 @@ const App: FunctionComponent<Props> = ({ error, title, state }) => {
return ( return (
<LocationProvider> <LocationProvider>
<AuthProvider user={state.user}>
<div id='app' className={s.base}> <div id='app' className={s.base}>
<Head> <Head>
<title>{title || 'BRF'}</title> <title>{title || 'Untitled'}</title>
</Head> </Head>
<Header routes={routes} /> <Header routes={routes} />
@ -78,16 +74,14 @@ const App: FunctionComponent<Props> = ({ error, title, state }) => {
) : ( ) : (
<Router onLoadStart={onLoadStart} onLoadEnd={onLoadEnd} onRouteChange={onRouteChange}> <Router onLoadStart={onLoadStart} onLoadEnd={onLoadEnd} onRouteChange={onRouteChange}>
{routes.map((route) => ( {routes.map((route) => (
<AuthRoute key={route.path} {...route} /> <Route key={route.path} path={route.path} component={route.component} />
))} ))}
<Refresh path='/admin' />
</Router> </Router>
)} )}
</main> </main>
<Footer /> <Footer />
</div> </div>
</AuthProvider>
</LocationProvider> </LocationProvider>
) )
} }

View File

@ -1,5 +0,0 @@
.links {
display: flex;
gap: 10px;
justify-content: end;
}

View File

@ -1,38 +0,0 @@
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

View File

@ -1,14 +1,6 @@
@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();
@ -19,16 +11,6 @@
display: block; display: block;
padding: 5px; padding: 5px;
} }
&:first-child {
> a {
padding-left: 0;
}
}
} }
} }
} }
.currentUser {
grid-area: 1 / 2 / 3 / 3;
}

View File

@ -1,11 +1,10 @@
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 className={s.base}> <header>
<h1>BRF Tegeltrasten</h1> <h1>BRF Tegeltrasten</h1>
<nav className={s.nav}> <nav className={s.nav}>
<ul> <ul>
@ -18,7 +17,6 @@ const Header: FunctionComponent<{ routes: Route[] }> = ({ routes }) => (
)} )}
</ul> </ul>
</nav> </nav>
<CurrentUser className={s.currentUser} />
</header> </header>
) )

View File

@ -1,69 +0,0 @@
@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);
}

View File

@ -1,5 +0,0 @@
import inputFactory from '../../shared/components/input_factory.tsx'
import s from './input.module.scss'
export default inputFactory(s)

View File

@ -1,30 +0,0 @@
.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;
}
}

View File

@ -1,77 +0,0 @@
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

View File

@ -1,10 +0,0 @@
import { h } from 'preact'
import LoginForm from './login_form.tsx'
const LoginPage = () => (
<section>
<LoginForm />
</section>
)
export default LoginPage

View File

@ -1,34 +0,0 @@
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)
}
}

View File

@ -71,9 +71,7 @@ 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> <td>{transaction.accountNumber}</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)

View File

@ -1,4 +1,3 @@
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'
@ -6,7 +5,6 @@ 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'
@ -18,29 +16,18 @@ 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',
@ -48,21 +35,18 @@ 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',
@ -70,7 +54,6 @@ 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',
@ -78,35 +61,23 @@ 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,
}, },
] ]

View File

@ -1,14 +1,3 @@
: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 {

View File

@ -1,15 +0,0 @@
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

View File

@ -2,7 +2,7 @@
-- PostgreSQL database dump -- PostgreSQL database dump
-- --
\restrict wU1dfnHqRTt2y70XvbaA1LDda0PgjnckSPP98WqYNl91ofZctLJ9wjIOEE8bpwR \restrict kNYhdwOhwE9I3bgAzdljyYgB5xyEpjhiaSCeYZfp84v3ey1GpvsdxX4U8Y8fQM3
-- 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: aliases_supplier; Type: TABLE; Schema: public; Owner: - -- Name: aliasesToSupplier; Type: TABLE; Schema: public; Owner: -
-- --
CREATE TABLE public.aliases_supplier ( CREATE TABLE public."aliasesToSupplier" (
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.aliases_supplier (
-- --
-- Name: aliases_supplier_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- Name: aliasesToSupplier_id_seq; Type: SEQUENCE; Schema: public; Owner: -
-- --
CREATE SEQUENCE public.aliases_supplier_id_seq CREATE SEQUENCE public."aliasesToSupplier_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.aliases_supplier_id_seq
-- --
-- Name: aliases_supplier_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- Name: aliasesToSupplier_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
-- --
ALTER SEQUENCE public.aliases_supplier_id_seq OWNED BY public.aliases_supplier.id; ALTER SEQUENCE public."aliasesToSupplier_id_seq" OWNED BY public."aliasesToSupplier".id;
-- --
@ -199,10 +199,10 @@ ALTER SEQUENCE public.file_id_seq OWNED BY public.file.id;
-- --
-- Name: files_invoice; Type: TABLE; Schema: public; Owner: - -- Name: filesToInvoice; Type: TABLE; Schema: public; Owner: -
-- --
CREATE TABLE public.files_invoice ( CREATE TABLE public."filesToInvoice" (
"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: transactions_objects; Type: TABLE; Schema: public; Owner: - -- Name: transactionsToObjects; Type: TABLE; Schema: public; Owner: -
-- --
CREATE TABLE public.transactions_objects ( CREATE TABLE public."transactionsToObjects" (
"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: aliases_supplier id; Type: DEFAULT; Schema: public; Owner: - -- Name: aliasesToSupplier id; Type: DEFAULT; Schema: public; Owner: -
-- --
ALTER TABLE ONLY public.aliases_supplier ALTER COLUMN id SET DEFAULT nextval('public.aliases_supplier_id_seq'::regclass); ALTER TABLE ONLY public."aliasesToSupplier" ALTER COLUMN id SET DEFAULT nextval('public."aliasesToSupplier_id_seq"'::regclass);
-- --
@ -560,19 +560,19 @@ ALTER TABLE ONLY public.account
-- --
-- Name: aliases_supplier aliases_supplier_alias_key; Type: CONSTRAINT; Schema: public; Owner: - -- Name: aliasesToSupplier aliasesToSupplier_alias_key; Type: CONSTRAINT; Schema: public; Owner: -
-- --
ALTER TABLE ONLY public.aliases_supplier ALTER TABLE ONLY public."aliasesToSupplier"
ADD CONSTRAINT aliases_supplier_alias_key UNIQUE (alias); ADD CONSTRAINT "aliasesToSupplier_alias_key" UNIQUE (alias);
-- --
-- Name: aliases_supplier aliases_supplier_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: aliasesToSupplier aliasesToSupplier_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
ALTER TABLE ONLY public.aliases_supplier ALTER TABLE ONLY public."aliasesToSupplier"
ADD CONSTRAINT aliases_supplier_pkey PRIMARY KEY (id); ADD CONSTRAINT "aliasesToSupplier_pkey" PRIMARY KEY (id);
-- --
@ -600,11 +600,11 @@ ALTER TABLE ONLY public.file
-- --
-- Name: files_invoice files_invoice_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: filesToInvoice filesToInvoice_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
ALTER TABLE ONLY public.files_invoice ALTER TABLE ONLY public."filesToInvoice"
ADD CONSTRAINT files_invoice_pkey PRIMARY KEY ("invoiceId", "fileId"); ADD CONSTRAINT "filesToInvoice_pkey" PRIMARY KEY ("invoiceId", "fileId");
-- --
@ -631,22 +631,6 @@ 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: -
-- --
@ -712,35 +696,35 @@ ALTER TABLE ONLY public.transaction
-- --
-- Name: transactions_objects transactions_objects_transactionId_objectId_key; Type: CONSTRAINT; Schema: public; Owner: - -- Name: transactionsToObjects transactionsToObjects_transactionId_objectId_key; Type: CONSTRAINT; Schema: public; Owner: -
-- --
ALTER TABLE ONLY public.transactions_objects ALTER TABLE ONLY public."transactionsToObjects"
ADD CONSTRAINT "transactions_objects_transactionId_objectId_key" UNIQUE ("transactionId", "objectId"); ADD CONSTRAINT "transactionsToObjects_transactionId_objectId_key" UNIQUE ("transactionId", "objectId");
-- --
-- Name: aliases_supplier aliases_supplier_supplierId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- Name: aliasesToSupplier aliasesToSupplier_supplierId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
-- --
ALTER TABLE ONLY public.aliases_supplier ALTER TABLE ONLY public."aliasesToSupplier"
ADD CONSTRAINT "aliases_supplier_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES public.supplier(id); ADD CONSTRAINT "aliasesToSupplier_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES public.supplier(id);
-- --
-- Name: files_invoice files_invoice_fileId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- Name: filesToInvoice filesToInvoice_fileId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
-- --
ALTER TABLE ONLY public.files_invoice ALTER TABLE ONLY public."filesToInvoice"
ADD CONSTRAINT "files_invoice_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES public.file(id); ADD CONSTRAINT "filesToInvoice_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES public.file(id);
-- --
-- Name: files_invoice files_invoice_invoiceId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- Name: filesToInvoice filesToInvoice_invoiceId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
-- --
ALTER TABLE ONLY public.files_invoice ALTER TABLE ONLY public."filesToInvoice"
ADD CONSTRAINT "files_invoice_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES public.invoice(id); ADD CONSTRAINT "filesToInvoice_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES public.invoice(id);
-- --
@ -800,24 +784,24 @@ ALTER TABLE ONLY public.transaction
-- --
-- Name: transactions_objects transactions_objects_objectId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- Name: transactionsToObjects transactionsToObjects_objectId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
-- --
ALTER TABLE ONLY public.transactions_objects ALTER TABLE ONLY public."transactionsToObjects"
ADD CONSTRAINT "transactions_objects_objectId_fkey" FOREIGN KEY ("objectId") REFERENCES public.object(id); ADD CONSTRAINT "transactionsToObjects_objectId_fkey" FOREIGN KEY ("objectId") REFERENCES public.object(id);
-- --
-- Name: transactions_objects transactions_objects_transactionId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- Name: transactionsToObjects transactionsToObjects_transactionId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
-- --
ALTER TABLE ONLY public.transactions_objects ALTER TABLE ONLY public."transactionsToObjects"
ADD CONSTRAINT "transactions_objects_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES public.transaction(id); ADD CONSTRAINT "transactionsToObjects_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES public.transaction(id);
-- --
-- PostgreSQL database dump complete -- PostgreSQL database dump complete
-- --
\unrestrict wU1dfnHqRTt2y70XvbaA1LDda0PgjnckSPP98WqYNl91ofZctLJ9wjIOEE8bpwR \unrestrict kNYhdwOhwE9I3bgAzdljyYgB5xyEpjhiaSCeYZfp84v3ey1GpvsdxX4U8Y8fQM3

View File

@ -20,11 +20,11 @@ auth_tables = [
accounting_tables = [ accounting_tables = [
'account', 'account',
'accountBalance', 'accountBalance',
'aliases_supplier', 'aliasesToSupplier',
'dimension', 'dimension',
'entry', 'entry',
'file', 'file',
'files_invoice', 'filesToInvoice',
'financialYear', 'financialYear',
'invoice', 'invoice',
'journal', 'journal',
@ -32,7 +32,7 @@ accounting_tables = [
'supplier', 'supplier',
'supplierType', 'supplierType',
'transaction', 'transaction',
'transactions_objects', 'transactionsToObjects',
] ]
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()

View File

@ -12,8 +12,7 @@
"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",
"fmt": "oxfmt", "format": "prettier --write .",
"fmt:check": "oxfmt --check",
"lint": "oxlint", "lint": "oxlint",
"prepare": "husky", "prepare": "husky",
"start": "node server/index.ts", "start": "node server/index.ts",
@ -29,48 +28,49 @@
"@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.3.2", "@fastify/middie": "^9.0.3",
"@fastify/session": "^11.1.1", "@fastify/session": "^11.1.1",
"@fastify/static": "^9.1.3", "@fastify/static": "^8.3.0",
"@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.8.5", "fastify": "^5.6.2",
"fastify-plugin": "^6.0.0", "fastify-plugin": "^5.1.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.11.1", "ioredis": "^5.8.2",
"kysely": "^0.29.2", "kysely": "^0.28.9",
"lodash": "^4.18.1", "lodash": "^4.17.21",
"lowline": "^0.4.2", "lowline": "^0.4.2",
"mini-qs": "^0.2.0", "mini-qs": "^0.2.0",
"pg": "^8.21.0", "pg": "^8.16.3",
"pg-protocol": "^1.14.0", "pg-protocol": "^1.10.3",
"pino-abstract-transport": "^3.0.0", "pino-abstract-transport": "^3.0.0",
"preact": "^10.29.2", "preact": "^10.28.0",
"preact-iso": "^2.12.0", "preact-iso": "^2.11.0",
"preact-router": "^4.1.2", "preact-router": "^4.1.2",
"rek": "^0.8.1", "rek": "^0.8.1",
"zod": "^4.4.3" "zod": "^4.2.1"
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "^2.10.5", "@babel/core": "^7.26.10",
"@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.24", "@types/lodash": "^4.17.16",
"@types/node": "^25.9.3", "@types/node": "^24.10.1",
"@typescript/native-preview": "7.0.0-dev.20260614.1", "@typescript/native-preview": "7.0.0-dev.20251126.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": "^29.1.1", "jsdom": "^27.2.0",
"lint-staged": "^17.0.7", "lint-staged": "^16.2.7",
"oxfmt": "^0.54.0", "oxlint": "^1.29.0",
"oxlint": "^1.69.0", "prettier": "^3.5.3",
"sass": "^1.101.0", "sass": "^1.85.1",
"typebox": "^1.2.10", "typebox": "^1.0.55",
"vite": "^8.0.16" "vite": "^7.2.4"
} }
} }

3894
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -26,9 +26,9 @@ const maxLoginAttempts = 5
const requireVerification = true const requireVerification = true
const redirects = { const redirects = {
login: '/', login: '/admin/',
logout: '/login', logout: '/admin/login',
register: '/login', register: '/admin/login',
} }
const remember = { const remember = {

View File

@ -321,7 +321,7 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
} }
await trx await trx
.insertInto('transactions_objects') .insertInto('transactionsToObjects')
.values({ .values({
transactionId, transactionId,
objectId, objectId,

View File

@ -1,6 +1,6 @@
import build from 'pino-abstract-transport' import build from 'pino-abstract-transport'
export default function dbTransport(create: ANY) { export default function dbTransport(create) {
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

View File

@ -74,13 +74,6 @@ 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)

View File

@ -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('/login') return reply.redirect('/admin/login')
} }
export default { export default {

View File

@ -3,7 +3,6 @@ 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
@ -21,88 +20,6 @@ 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()
} }

View File

@ -31,7 +31,7 @@ const invoiceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
jsonArrayFrom( jsonArrayFrom(
eb eb
.selectFrom('file as f') .selectFrom('file as f')
.innerJoin('files_invoice as fi', 'f.id', 'fi.fileId') .innerJoin('filesToInvoice 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('files_invoice as fi', 'f.id', 'fi.fileId') .innerJoin('filesToInvoice 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'),

View File

@ -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('transactions_objects as to', 't.id', 'to.transactionId') .innerJoin('transactionsToObjects 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)

View File

@ -48,19 +48,6 @@ 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: '/',
@ -69,6 +56,15 @@ 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')
}
}
},
}, },
}, },
}) })

View File

@ -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,9 +38,6 @@ 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> <script>var offset=window.history.state&&window.history.state.scrollTop||0;if(offset)window.scrollTo(0,offset)</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
View File

@ -13,7 +13,6 @@ declare global {
declare module 'fastify' { declare module 'fastify' {
interface Session { interface Session {
lastVisit?: string
userId: number userId: number
} }

View File

@ -55,7 +55,7 @@ export interface AdmissionsRoles {
roleId: number roleId: number
} }
export interface AliasesSupplier { export interface AliasesToSupplier {
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 FilesInvoice { export interface FilesToInvoice {
fileId: number fileId: number
invoiceId: number invoiceId: number
} }
@ -206,7 +206,7 @@ export interface Transaction {
transactionDate: Timestamp | null transactionDate: Timestamp | null
} }
export interface TransactionsObjects { export interface TransactionsToObjects {
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
aliases_Supplier: AliasesSupplier aliasesToSupplier: AliasesToSupplier
dimension: Dimension dimension: Dimension
emailToken: EmailToken emailToken: EmailToken
entry: Entry entry: Entry
error: Error error: Error
file: File file: File
files_invoice: FilesInvoice filesToInvoice: FilesToInvoice
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
transactions_objects: TransactionsObjects transactionsToObjects: TransactionsToObjects
user: User user: User
users_roles: UsersRoles users_roles: UsersRoles
} }