add transactions and random fixes
This commit is contained in:
parent
fa4563f771
commit
e5bbd266e9
15
.bruno/BRF/api-transactions.bru
Normal file
15
.bruno/BRF/api-transactions.bru
Normal file
@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: /api/transactions
|
||||
type: http
|
||||
seq: 14
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/transactions
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
@ -20,7 +20,7 @@ const AccountsPage = () => {
|
||||
</Head>
|
||||
|
||||
<h1>Accounts</h1>
|
||||
<table>
|
||||
<table className='grid'>
|
||||
<thead>
|
||||
<th>Number</th>
|
||||
<th>Description</th>
|
||||
|
||||
@ -1,14 +1,43 @@
|
||||
import { h } from 'preact'
|
||||
import { useCallback, useEffect } from 'preact/hooks'
|
||||
import { LocationProvider, Route, Router } from 'preact-iso'
|
||||
import { get } from 'lowline'
|
||||
import Head from './head.ts'
|
||||
import Footer from './footer.tsx'
|
||||
import Header from './header.tsx'
|
||||
import ErrorPage from './error_page.tsx'
|
||||
import routes from '../routes.ts'
|
||||
import throttle from '../../shared/utils/throttle.ts'
|
||||
|
||||
import s from './app.module.scss'
|
||||
|
||||
const remember = throttle(function remember() {
|
||||
window.history.replaceState(
|
||||
{
|
||||
...window.history.state,
|
||||
scrollTop: window.scrollY,
|
||||
},
|
||||
'',
|
||||
null,
|
||||
)
|
||||
}, 100)
|
||||
|
||||
export default function App({ error, url, title }) {
|
||||
useEffect(() => {
|
||||
addEventListener('scroll', remember)
|
||||
|
||||
return () => removeEventListener('scroll', remember)
|
||||
})
|
||||
|
||||
const onRouteChange = useCallback(() => {
|
||||
const offset = get(window.history, 'state.scrollTop')
|
||||
console.log('offset', offset)
|
||||
|
||||
setTimeout(() => {
|
||||
window.scrollTo(0, offset || 0)
|
||||
}, 100)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<LocationProvider>
|
||||
<div id='app' className={s.base}>
|
||||
@ -22,7 +51,7 @@ export default function App({ error, url, title }) {
|
||||
{error ? (
|
||||
<ErrorPage error={error} />
|
||||
) : (
|
||||
<Router>
|
||||
<Router onRouteChange={onRouteChange}>
|
||||
{routes.map((route) => (
|
||||
<Route key={route.path} route={route} path={route.path} component={route.component} />
|
||||
))}
|
||||
|
||||
@ -34,7 +34,7 @@ const EntriesPage = () => {
|
||||
setJournals(journals)
|
||||
})
|
||||
rek('/api/financial-years').then((financialYears) => {
|
||||
setFinancialYears(financialYears.toReversed())
|
||||
setFinancialYears(financialYears)
|
||||
})
|
||||
}, [])
|
||||
|
||||
@ -68,7 +68,7 @@ const EntriesPage = () => {
|
||||
<h2>
|
||||
{selectedJournal} : {selectedYear}
|
||||
</h2>
|
||||
<table>
|
||||
<table className='grid'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
@ -85,10 +85,13 @@ const EntriesPage = () => {
|
||||
<td>
|
||||
<a href={`/entries/${entry.id}`}>{entry.id}</a>
|
||||
</td>
|
||||
<td>{entry.number}</td>
|
||||
<td>
|
||||
{entry.journal}
|
||||
{entry.number}
|
||||
</td>
|
||||
<td>{entry.entryDate?.slice(0, 10)}</td>
|
||||
<td>{entry.transactionDate?.slice(0, 10)}</td>
|
||||
<td>{entry.amount}</td>
|
||||
<td className='tar'>{entry.amount}</td>
|
||||
<td>{entry.description}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@ -11,10 +11,6 @@ const EntriesPage = () => {
|
||||
const location = useLocation()
|
||||
const route = useRoute()
|
||||
|
||||
// console.dir(route)
|
||||
// console.dir(location)
|
||||
console.log(entry)
|
||||
|
||||
useEffect(() => {
|
||||
rek(`/api/entries/${route.params.id}`).then((entry) => {
|
||||
setEntry(entry)
|
||||
@ -23,8 +19,6 @@ const EntriesPage = () => {
|
||||
|
||||
if (!entry) return
|
||||
|
||||
console.log(entry)
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Head>
|
||||
@ -37,7 +31,7 @@ const EntriesPage = () => {
|
||||
Entry {entry.journal} {entry.number}
|
||||
</h1>
|
||||
|
||||
<table>
|
||||
<table className='grid'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
@ -58,14 +52,14 @@ const EntriesPage = () => {
|
||||
<td>{entry.number}</td>
|
||||
<td>{entry.entryDate?.slice(0, 10)}</td>
|
||||
<td>{entry.transactionDate?.slice(0, 10)}</td>
|
||||
<td>{entry.amount}</td>
|
||||
<td className='tar'>{entry.amount}</td>
|
||||
<td>{entry.description}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Transactions</h2>
|
||||
<table>
|
||||
<table className='grid'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account</th>
|
||||
@ -78,8 +72,8 @@ const EntriesPage = () => {
|
||||
{entry?.transactions?.map((transaction) => (
|
||||
<tr>
|
||||
<td>{transaction.account_number}</td>
|
||||
<td>{transaction.amount >= 0 ? formatNumber(transaction.amount) : null}</td>
|
||||
<td>{transaction.amount < 0 ? formatNumber(Math.abs(transaction.amount)) : null}</td>
|
||||
<td className='tar'>{transaction.amount >= 0 ? formatNumber(transaction.amount) : null}</td>
|
||||
<td className='tar'>{transaction.amount < 0 ? formatNumber(Math.abs(transaction.amount)) : null}</td>
|
||||
<td>{transaction.description}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
16
client/public/components/header.module.scss
Normal file
16
client/public/components/header.module.scss
Normal file
@ -0,0 +1,16 @@
|
||||
@use '../../shared/styles/utils';
|
||||
|
||||
.nav {
|
||||
> ul {
|
||||
@include utils.wipe-list();
|
||||
|
||||
display: flex;
|
||||
|
||||
> li {
|
||||
> a {
|
||||
display: block;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
import { h } from 'preact'
|
||||
import s from './header.module.scss'
|
||||
|
||||
const Header = ({ routes }) => (
|
||||
<header>
|
||||
<h1>BRF Tegeltrasten</h1>
|
||||
<nav>
|
||||
<nav className={s.nav}>
|
||||
<ul>
|
||||
{routes.map((route) =>
|
||||
route.nav !== false ? (
|
||||
|
||||
@ -14,13 +14,24 @@ const Result: FunctionalComponent<Props> = ({ objectId }) => {
|
||||
}, [objectId])
|
||||
|
||||
return (
|
||||
<table>
|
||||
<table className='grid'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Entry</th>
|
||||
<th>Date</th>
|
||||
<th>Account</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map((transaction) => (
|
||||
<tr>
|
||||
<td>
|
||||
<a href={`/entries/${transaction.entryId}`}>{transaction.entryId}</a>
|
||||
</td>
|
||||
<td>{transaction.transactionDate.slice(0, 10)}</td>
|
||||
<td>{transaction.accountNumber}</td>
|
||||
<td>{transaction.amount}</td>
|
||||
<td className='tar'>{transaction.amount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@ -1,16 +1,9 @@
|
||||
.table {
|
||||
td:nth-child(3),
|
||||
td:nth-child(3) ~ td {
|
||||
text-align: right;
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
&:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border: 1px solid #ccc;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { h } from 'preact'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import cn from 'classnames'
|
||||
import rek from 'rek'
|
||||
import Head from './head.ts'
|
||||
import Result from './result.tsx'
|
||||
@ -13,7 +14,7 @@ const ResultsPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
rek(`/api/results`).then(setResults)
|
||||
rek(`/api/financial-years`).then((years) => setYears(years.map((fy) => fy.year).toReversed()))
|
||||
rek(`/api/financial-years`).then((years) => setYears(years.map((fy) => fy.year)))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@ -24,7 +25,7 @@ const ResultsPage = () => {
|
||||
|
||||
<h1>Results</h1>
|
||||
{years.length && results.length && (
|
||||
<table className={s.table}>
|
||||
<table className={cn('grid', s.table)}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account</th>
|
||||
@ -40,7 +41,11 @@ const ResultsPage = () => {
|
||||
<td>{result.accountNumber}</td>
|
||||
<td>{result.description}</td>
|
||||
{years.map((year) => (
|
||||
<td>{formatNumber(result[year])}</td>
|
||||
<td className='tar'>
|
||||
<a href={`/transactions?year=${year}&accountNumber=${result.accountNumber}`}>
|
||||
{formatNumber(result[year])}
|
||||
</a>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
||||
96
client/public/components/transactions_page.tsx
Normal file
96
client/public/components/transactions_page.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { h } from 'preact'
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks'
|
||||
import { useLocation } from 'preact-iso'
|
||||
import { isEmpty } from 'lowline'
|
||||
import qs from 'mini-qs'
|
||||
import rek from 'rek'
|
||||
import Head from './head.ts'
|
||||
import serializeForm from '../../shared/utils/serialize_form.ts'
|
||||
import { formatNumber } from '../utils/format_number.ts'
|
||||
|
||||
const TransactionsPage = () => {
|
||||
const [financialYears, setFinancialYears] = useState([])
|
||||
const [transactions, setTransactions] = useState([])
|
||||
|
||||
const location = useLocation()
|
||||
|
||||
console.log(location)
|
||||
|
||||
const onSubmit = useCallback((e: SubmitEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const values = serializeForm(e.target as HTMLFormElement)
|
||||
|
||||
const search = !isEmpty(values) ? '?' + qs.stringify(values) : ''
|
||||
|
||||
location.route(`/transactions${search}`)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
rek('/api/financial-years').then((financialYears) => {
|
||||
setFinancialYears(financialYears)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const search = location.url.split('?')[1] || ''
|
||||
|
||||
rek(`/api/transactions${search ? '?' + search : ''}`).then((transactions) => {
|
||||
setTransactions(transactions)
|
||||
})
|
||||
}, [location.url])
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Head>
|
||||
<title> : Transactions</title>
|
||||
</Head>
|
||||
|
||||
<form onSubmit={onSubmit}>
|
||||
<select name='year' defaultValue={location.query.year || '2025'}>
|
||||
{financialYears.map((financialYear) => (
|
||||
<option value={financialYear.year}>{financialYear.year}</option>
|
||||
))}
|
||||
</select>
|
||||
<input type='text' name='accountNumber' defaultValue={location.query.accountNumber || ''} />
|
||||
<button>Search</button>
|
||||
</form>
|
||||
|
||||
<h1>Transactions</h1>
|
||||
<div>
|
||||
<table className='grid'>
|
||||
<thead>
|
||||
<th>Date</th>
|
||||
<th>Account</th>
|
||||
<th>Debit</th>
|
||||
<th>Credit</th>
|
||||
<th>Description</th>
|
||||
<th>Entry</th>
|
||||
<th>Entry Description</th>
|
||||
<th>Invoice</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map((transaction) => (
|
||||
<tr>
|
||||
<td>{transaction.transactionDate.slice(0, 10)}</td>
|
||||
<td>{transaction.accountNumber}</td>
|
||||
<td className='tar'>{transaction.amount >= 0 ? formatNumber(transaction.amount) : null}</td>
|
||||
<td className='tar'>{transaction.amount < 0 ? formatNumber(Math.abs(transaction.amount)) : null}</td>
|
||||
<td>{transaction.description}</td>
|
||||
<td>
|
||||
<a href={`/entries/${transaction.entryId}`}>{transaction.entryId}</a>
|
||||
</td>
|
||||
<td>{transaction.entryDescription}</td>
|
||||
<td>
|
||||
<a href={`/invoices/${transaction.invoiceId}`}>{transaction.invoiceId}</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default TransactionsPage
|
||||
@ -7,6 +7,7 @@ import InvoicesBySupplier from './components/invoices_by_supplier_page.tsx'
|
||||
import Objects from './components/objects_page.tsx'
|
||||
import Results from './components/results_page.tsx'
|
||||
import Start from './components/start_page.tsx'
|
||||
import Transactions from './components/transactions_page.tsx'
|
||||
|
||||
export default [
|
||||
{
|
||||
@ -66,4 +67,10 @@ export default [
|
||||
title: 'Results',
|
||||
component: Results,
|
||||
},
|
||||
{
|
||||
path: '/transactions',
|
||||
name: 'transactions',
|
||||
title: 'Transactions',
|
||||
component: Transactions,
|
||||
},
|
||||
]
|
||||
|
||||
@ -7,4 +7,20 @@
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
|
||||
&.grid {
|
||||
tr:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border: 1px solid #ccc;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tar {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
24
client/shared/utils/throttle.ts
Normal file
24
client/shared/utils/throttle.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// copied and modified from https://github.com/sindresorhus/throttleit/blob/ed1d22c70a964ef0299d0400dbfd1fbedef56a59/index.js
|
||||
|
||||
export default function throttle(fnc, wait) {
|
||||
let timeout
|
||||
let lastCallTime = 0
|
||||
|
||||
return function throttled(...args) {
|
||||
clearTimeout(timeout)
|
||||
|
||||
const now = Date.now()
|
||||
const timeSinceLastCall = now - lastCallTime
|
||||
const delayForNextCall = wait - timeSinceLastCall
|
||||
|
||||
if (delayForNextCall <= 0) {
|
||||
lastCallTime = now
|
||||
fnc(...args)
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
lastCallTime = Date.now()
|
||||
fnc(...args)
|
||||
}, delayForNextCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import journals from './api/journals.ts'
|
||||
import objects from './api/objects.ts'
|
||||
import results from './api/results.ts'
|
||||
import suppliers from './api/suppliers.ts'
|
||||
import transactions from './api/transactions.ts'
|
||||
|
||||
export const FinancialYear = Type.Object({
|
||||
year: Type.Number(),
|
||||
@ -28,6 +29,7 @@ const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
||||
fastify.register(objects, { prefix: '/objects' })
|
||||
fastify.register(results, { prefix: '/results' })
|
||||
fastify.register(suppliers, { prefix: '/suppliers' })
|
||||
fastify.register(transactions, { prefix: '/transactions' })
|
||||
|
||||
done()
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ const financialYearRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) =>
|
||||
url: '/',
|
||||
method: 'GET',
|
||||
handler() {
|
||||
return knex('financialYear').select('*')
|
||||
return knex('financialYear').select('*').orderBy('startDate', 'desc')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ const objectRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
||||
},
|
||||
async handler(req) {
|
||||
return knex('transaction AS t')
|
||||
.select('e.transactionDate', 't.accountNumber', 't.amount')
|
||||
.select('t.entryId', 'e.transactionDate', 't.accountNumber', 't.amount')
|
||||
.innerJoin('transactions_to_objects AS to', function () {
|
||||
this.on('t.id', 'to.transactionId')
|
||||
})
|
||||
|
||||
61
server/routes/api/transactions.ts
Normal file
61
server/routes/api/transactions.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import _ from 'lodash'
|
||||
import { Type, type Static, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
||||
import knex from '../../lib/knex.ts'
|
||||
import StatusError from '../../lib/status_error.ts'
|
||||
|
||||
const transactionRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
||||
fastify.route({
|
||||
url: '/',
|
||||
method: 'GET',
|
||||
schema: {
|
||||
querystring: Type.Object({
|
||||
year: Type.Optional(Type.Number()),
|
||||
accountNumber: Type.Optional(Type.Number()),
|
||||
}),
|
||||
},
|
||||
async handler(req) {
|
||||
const query: { financialYearId?: number; accountNumber?: number } = {}
|
||||
|
||||
if (req.query.year) {
|
||||
const year = await knex('financialYear').first('*').where('year', req.query.year)
|
||||
|
||||
if (!year) throw new StatusError(404, `Year ${req.query.year} not found.`)
|
||||
query.financialYearId = year.id
|
||||
}
|
||||
|
||||
if (req.query.accountNumber) {
|
||||
query.accountNumber = req.query.accountNumber
|
||||
}
|
||||
|
||||
return knex('transaction AS t')
|
||||
.select(
|
||||
't.accountNumber',
|
||||
'e.transactionDate',
|
||||
't.entryId',
|
||||
't.amount',
|
||||
't.description',
|
||||
't.invoiceId',
|
||||
'e.description AS entryDescription',
|
||||
)
|
||||
.innerJoin('entry AS e', 't.entry_id', 'e.id')
|
||||
.where(query)
|
||||
},
|
||||
})
|
||||
|
||||
fastify.route({
|
||||
url: '/:id',
|
||||
method: 'GET',
|
||||
schema: {
|
||||
params: Type.Object({
|
||||
id: Type.Number(),
|
||||
}),
|
||||
},
|
||||
async handler(req) {
|
||||
return knex('transaction').first('*').where('id', req.params.id)
|
||||
},
|
||||
})
|
||||
|
||||
done()
|
||||
}
|
||||
|
||||
export default transactionRoutes
|
||||
Loading…
Reference in New Issue
Block a user