results and other stuffs

This commit is contained in:
Linus Miller 2025-12-03 16:58:22 +01:00
parent 723d8840c8
commit fa4563f771
33 changed files with 585 additions and 100 deletions

View File

@ -0,0 +1,20 @@
meta {
name: /api/entries/:id
type: http
seq: 4
}
get {
url: {{base_url}}/api/entries/:id
body: none
auth: inherit
}
params:path {
id:
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

@ -5,11 +5,15 @@ meta {
}
get {
url: {{base_url}}/api/invoices/2631
url: {{base_url}}/api/invoices/:id
body: none
auth: inherit
}
params:path {
id: 1000
}
settings {
encodeUrl: true
timeout: 0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,101 @@
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'
const dateYear = new Date().getFullYear()
const EntriesPage = () => {
const [journals, setJournals] = useState([])
const [financialYears, setFinancialYears] = useState([])
const [entries, setEntries] = useState([])
const location = useLocation()
const { journal: selectedJournal = 'A', year: selectedYear = dateYear } = location.query
const onSubmit = useCallback((e: SubmitEvent) => {
e.preventDefault()
const values = serializeForm(e.target as HTMLFormElement)
const search = !isEmpty(values) ? '?' + qs.stringify(values) : ''
location.route(`/entries${search}`)
}, [])
useEffect(() => {
rek('/api/journals').then((journals) => {
setJournals(journals)
})
rek('/api/financial-years').then((financialYears) => {
setFinancialYears(financialYears.toReversed())
})
}, [])
useEffect(() => {
rek(`/api/entries?journal=${selectedJournal}&year=${selectedYear}`).then((entries) => setEntries(entries))
}, [selectedJournal, selectedYear])
return financialYears.length && journals.length ? (
<section>
<Head>
<title> : Entries</title>
</Head>
<h1>Entries</h1>
<form onSubmit={onSubmit}>
<select defaultValue='D' name='journal'>
{/*<option value='A'>A</option>
<option value='D'>D</option>*/}
{journals.map((journal) => (
<option value={journal.identifier}>{journal.identifier}</option>
))}
</select>
<select name='year' defaultValue={selectedYear}>
{financialYears.map((financialYear) => (
<option value={financialYear.year}>{financialYear.year}</option>
))}
</select>
<button>Search</button>
</form>
<h2>
{selectedJournal} : {selectedYear}
</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Number</th>
<th>Entry Date</th>
<th>Transaction Date</th>
<th>Amount</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{entries?.map((entry) => (
<tr>
<td>
<a href={`/entries/${entry.id}`}>{entry.id}</a>
</td>
<td>{entry.number}</td>
<td>{entry.entryDate?.slice(0, 10)}</td>
<td>{entry.transactionDate?.slice(0, 10)}</td>
<td>{entry.amount}</td>
<td>{entry.description}</td>
</tr>
))}
</tbody>
</table>
</section>
) : null
}
export default EntriesPage

View File

@ -0,0 +1,92 @@
import { h } from 'preact'
import { useEffect, useState } from 'preact/hooks'
import { useRoute, useLocation } from 'preact-iso'
import { formatNumber } from '../utils/format_number.ts'
import rek from 'rek'
import Head from './head.ts'
const EntriesPage = () => {
const [entry, setEntry] = useState([])
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)
})
}, [])
if (!entry) return
console.log(entry)
return (
<section>
<Head>
<title>
{' '}
: Entry {entry.journal} {entry.number}{' '}
</title>
</Head>
<h1>
Entry {entry.journal} {entry.number}
</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Journal</th>
<th>Number</th>
<th>Entry Date</th>
<th>Transaction Date</th>
<th>Amount</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a href={`/entries/${entry.id}`}>{entry.id}</a>
</td>
<td>{entry.journal}</td>
<td>{entry.number}</td>
<td>{entry.entryDate?.slice(0, 10)}</td>
<td>{entry.transactionDate?.slice(0, 10)}</td>
<td>{entry.amount}</td>
<td>{entry.description}</td>
</tr>
</tbody>
</table>
<h2>Transactions</h2>
<table>
<thead>
<tr>
<th>Account</th>
<th>Debit</th>
<th>Credit</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{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>{transaction.description}</td>
</tr>
))}
</tbody>
</table>
</section>
)
}
export default EntriesPage

View File

@ -78,6 +78,7 @@ const InvoicePage = () => {
<table>
<thead>
<tr>
<th>Entry</th>
<th>Account</th>
<th>Debit</th>
<th>Credit</th>
@ -87,6 +88,9 @@ const InvoicePage = () => {
<tbody>
{invoice?.transactions?.map((transaction) => (
<tr>
<td>
<a href={`/entries/${transaction.entry_id}`}>{transaction.entry_id}</a>
</td>
<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>

View File

@ -10,12 +10,16 @@ const format = Format.bind(null, null, 'YYYY.MM.DD')
const InvoicesPage = () => {
const [supplier, setSupplier] = useState(null)
const [invoices, setInvoices] = useState([])
const [totalAmount, setTotalAmount] = useState<number>(null)
const route = useRoute()
useEffect(() => {
rek(`/api/suppliers/${route.params.supplier}`).then((supplier) => setSupplier(supplier))
rek(`/api/invoices?supplier=${route.params.supplier}`).then((invoices) => setInvoices(invoices))
rek(`/api/invoices/total-amount?supplier=${route.params.supplier}`).then((totalAmount) =>
setTotalAmount(totalAmount.amount),
)
}, [route.params.supplier])
return (
@ -26,6 +30,10 @@ const InvoicesPage = () => {
<h1>Invoices for {supplier?.name}</h1>
<p>
<strong>Total: {totalAmount}</strong>
</p>
<table>
<thead>
<tr>

View File

@ -28,7 +28,9 @@ const InvoicesPage = () => {
<ul>
{suppliers?.map((supplier) => (
<li>
<a href={`/invoices/by-supplier/${supplier.id}`}>{supplier.name}</a>
<a href={`/invoices/by-supplier/${supplier.id}`}>
({supplier.id}) {supplier.name}
</a>
</li>
))}
</ul>

View File

@ -1,31 +0,0 @@
import { h, type FunctionalComponent } from 'preact'
import { useEffect, useState } from 'preact/hooks'
import rek from 'rek'
interface Props {
year: number
}
const Result: FunctionalComponent<Props> = ({ year }) => {
const [result, setResults] = useState([])
useEffect(() => {
rek(`/api/results/${year}`).then(setResults)
}, [year])
return (
<table>
<tbody>
{result.map((result) => (
<tr>
<td>{result.accountNumber}</td>
<td>{result.description}</td>
<td>{result.amount}</td>
</tr>
))}
</tbody>
</table>
)
}
export default Result

View File

@ -1,5 +1,16 @@
.years {
display: flex;
flex-direction: row-reverse;
justify-content: start;
.table {
td:nth-child(3),
td:nth-child(3) ~ td {
text-align: right;
}
tr:hover {
background: #eee;
}
td,
th {
border: 1px solid #ccc;
padding: 3px 5px;
}
}

View File

@ -3,17 +3,17 @@ import { useEffect, useState } from 'preact/hooks'
import rek from 'rek'
import Head from './head.ts'
import Result from './result.tsx'
import Results from './results.tsx'
import { formatNumber } from '../utils/format_number.ts'
import s from './results_page.module.scss'
const ResultsPage = () => {
const [financialYears, setFinancialYears] = useState([])
const [currentYear, setCurrentYear] = useState<number>(null)
const [results, setResults] = useState([])
const [years, setYears] = useState<number[]>([])
useEffect(() => {
rek('/api/financial-years').then((financialYears) => {
setFinancialYears(financialYears)
setCurrentYear(financialYears[financialYears.length - 1].year)
})
rek(`/api/results`).then(setResults)
rek(`/api/financial-years`).then((years) => setYears(years.map((fy) => fy.year).toReversed()))
}, [])
return (
@ -23,17 +23,30 @@ const ResultsPage = () => {
</Head>
<h1>Results</h1>
<div className={s.years}>
{financialYears.map((financialYear) => (
<button onClick={() => setCurrentYear(financialYear.year)}>{financialYear.year}</button>
))}
</div>
{currentYear ? (
<div>
<h2>{currentYear}</h2>
<Result year={currentYear} />
</div>
) : null}
{years.length && results.length && (
<table className={s.table}>
<thead>
<tr>
<th>Account</th>
<th>Description</th>
{years.map((year) => (
<th>{year}</th>
))}
</tr>
</thead>
<tbody>
{results.map((result) => (
<tr>
<td>{result.accountNumber}</td>
<td>{result.description}</td>
{years.map((year) => (
<td>{formatNumber(result[year])}</td>
))}
</tr>
))}
</tbody>
</table>
)}
</section>
)
}

View File

@ -0,0 +1,3 @@
import selectFactory from '../../shared/components/select_factory.tsx'
export default selectFactory({ styles: {} })

View File

@ -1,4 +1,6 @@
import Accounts from './components/accounts_page.tsx'
import Entries from './components/entries_page.tsx'
import Entry from './components/entry_page.tsx'
import Invoice from './components/invoice_page.tsx'
import Invoices from './components/invoices_page.tsx'
import InvoicesBySupplier from './components/invoices_by_supplier_page.tsx'
@ -19,6 +21,19 @@ export default [
title: 'Accounts',
component: Accounts,
},
{
path: '/entries',
name: 'entries',
title: 'Entries',
component: Entries,
},
{
path: '/entries/:id',
name: 'entry',
title: 'Entry',
component: Entry,
nav: false,
},
{
path: '/objects',
name: 'objects',

View File

@ -3,3 +3,8 @@
*:after {
box-sizing: border-box;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

View File

@ -9,8 +9,8 @@ export const formatPrice = (price) => priceFormatter.format(price)
const numberFormatter = new Intl.NumberFormat('sv-SE', {
style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
export const formatNumber = (nbr) => numberFormatter.format(nbr)

View File

@ -46,6 +46,8 @@ export default function selectFactory({ styles }): FunctionComponent<{
const [touched, setTouched] = useState(false)
const selectRef = useRef<HTMLSelectElement>()
console.log(options)
const onBlur = useCallback(() => setTouched(true), [])
useEffect(() => {

View File

@ -2,7 +2,7 @@
-- PostgreSQL database dump
--
\restrict LKaLyA1IGSZwb31KR2e5GrFZPB7KuPkTsiIxHVwV0aqqukXTaBdKHj8rPqgoMsg
\restrict FugYqvehfvYcZV6n0VXYfKK3pEfWehcjXHsTSddhC5Qcn0530oCENplg6a2CdZd
-- Dumped from database version 18.1
-- Dumped by pg_dump version 18.1
@ -89,6 +89,37 @@ CREATE SEQUENCE public.account_id_seq
ALTER SEQUENCE public.account_id_seq OWNED BY public.account.id;
--
-- Name: aliases_to_supplier; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.aliases_to_supplier (
id integer NOT NULL,
supplier_id integer NOT NULL,
alias text NOT NULL
);
--
-- Name: aliases_to_supplier_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.aliases_to_supplier_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: aliases_to_supplier_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.aliases_to_supplier_id_seq OWNED BY public.aliases_to_supplier.id;
--
-- Name: dimension; Type: TABLE; Schema: public; Owner: -
--
@ -336,7 +367,8 @@ ALTER SEQUENCE public.object_id_seq OWNED BY public.object.id;
CREATE TABLE public.supplier (
id integer NOT NULL,
name text,
supplier_type_id integer NOT NULL
supplier_type_id integer NOT NULL,
tax_id text
);
@ -445,6 +477,13 @@ CREATE TABLE public.transactions_to_objects (
ALTER TABLE ONLY public.account ALTER COLUMN id SET DEFAULT nextval('public.account_id_seq'::regclass);
--
-- Name: aliases_to_supplier id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.aliases_to_supplier ALTER COLUMN id SET DEFAULT nextval('public.aliases_to_supplier_id_seq'::regclass);
--
-- Name: dimension id; Type: DEFAULT; Schema: public; Owner: -
--
@ -539,6 +578,22 @@ ALTER TABLE ONLY public.account
ADD CONSTRAINT account_pkey PRIMARY KEY (id);
--
-- Name: aliases_to_supplier aliases_to_supplier_alias_key; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.aliases_to_supplier
ADD CONSTRAINT aliases_to_supplier_alias_key UNIQUE (alias);
--
-- Name: aliases_to_supplier aliases_to_supplier_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.aliases_to_supplier
ADD CONSTRAINT aliases_to_supplier_pkey PRIMARY KEY (id);
--
-- Name: dimension dimension_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -635,6 +690,14 @@ ALTER TABLE ONLY public.supplier
ADD CONSTRAINT supplier_pkey PRIMARY KEY (id);
--
-- Name: supplier supplier_tax_id_key; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.supplier
ADD CONSTRAINT supplier_tax_id_key UNIQUE (tax_id);
--
-- Name: supplier_type supplier_type_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -659,6 +722,14 @@ ALTER TABLE ONLY public.transactions_to_objects
ADD CONSTRAINT transactions_to_objects_transaction_id_object_id_key UNIQUE (transaction_id, object_id);
--
-- Name: aliases_to_supplier aliases_to_supplier_supplier_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.aliases_to_supplier
ADD CONSTRAINT aliases_to_supplier_supplier_id_fkey FOREIGN KEY (supplier_id) REFERENCES public.supplier(id);
--
-- Name: files_to_invoice files_to_invoice_file_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -751,5 +822,5 @@ ALTER TABLE ONLY public.transactions_to_objects
-- PostgreSQL database dump complete
--
\unrestrict LKaLyA1IGSZwb31KR2e5GrFZPB7KuPkTsiIxHVwV0aqqukXTaBdKHj8rPqgoMsg
\unrestrict FugYqvehfvYcZV6n0VXYfKK3pEfWehcjXHsTSddhC5Qcn0530oCENplg6a2CdZd

View File

@ -29,12 +29,14 @@
"@fastify/static": "^8.3.0",
"@fastify/type-provider-typebox": "^6.1.0",
"chalk": "^5.6.2",
"classnames": "^2.5.1",
"easy-tz": "^0.2.0",
"fastify": "^5.6.2",
"fastify-plugin": "^5.1.0",
"knex": "^3.1.0",
"lodash": "^4.17.21",
"lowline": "^0.4.2",
"mini-qs": "^0.2.0",
"pg": "^8.16.3",
"pg-protocol": "^1.10.3",
"pino-abstract-transport": "^3.0.0",

16
pnpm-lock.yaml generated
View File

@ -26,6 +26,9 @@ importers:
chalk:
specifier: ^5.6.2
version: 5.6.2
classnames:
specifier: ^2.5.1
version: 2.5.1
easy-tz:
specifier: ^0.2.0
version: 0.2.0
@ -44,6 +47,9 @@ importers:
lowline:
specifier: ^0.4.2
version: 0.4.2
mini-qs:
specifier: ^0.2.0
version: 0.2.0
pg:
specifier: ^8.16.3
version: 8.16.3
@ -1085,6 +1091,9 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
@ -1687,6 +1696,9 @@ packages:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
mini-qs@0.2.0:
resolution: {integrity: sha512-4hc/KDREjho6ApJC139+NS6YSiSMxz1D6nYTSdDzgSIRrJvIR2+o8qgp9ABMaO0VJgeImgAsUpc+vQArWdFiFQ==}
minimatch@10.1.1:
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
engines: {node: 20 || >=22}
@ -3091,6 +3103,8 @@ snapshots:
dependencies:
readdirp: 4.1.2
classnames@2.5.1: {}
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
@ -3751,6 +3765,8 @@ snapshots:
mimic-function@5.0.1: {}
mini-qs@0.2.0: {}
minimatch@10.1.1:
dependencies:
'@isaacs/brace-expansion': 5.0.0

View File

@ -40,7 +40,7 @@ const defaultDecoder = {
export default async function parseStream(stream: ReadableStream, decoder: Decoder = defaultDecoder) {
const journals = new Map()
let currentEntryId: number
let currentEntry: { id: number; description: string }
let currentInvoiceId: number
let currentYear = null
const details: Record<string, string> = {}
@ -217,6 +217,10 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
await trx('invoice').insert({ financialYearId: currentYear.id, fiskenNumber, supplierId }).returning('id')
)[0]?.id
}
if (transaction.accountNumber === 2441 && currentEntry.description?.includes('Fakturajournal')) {
await trx('invoice').update('amount', Math.abs(transaction.amount)).where('id', invoiceId)
}
}
if (invoiceId && currentInvoiceId) {
@ -226,7 +230,7 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
const transactionId = (
await trx('transaction')
.insert({
entryId: currentEntryId,
entryId: currentEntry.id,
...transaction,
invoiceId: invoiceId || currentInvoiceId,
})
@ -313,15 +317,16 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
currentInvoiceId = null
}
currentEntryId = (
currentEntry = (
await trx('entry')
.insert({
journalId,
financialYearId: currentYear!.id,
...rest,
})
.returning('id')
)[0].id
.returning(['id', 'description'])
)[0]
console.log(currentEntry)
break
}

View File

@ -9,6 +9,7 @@ import invoices from './api/invoices.ts'
import journals from './api/journals.ts'
import objects from './api/objects.ts'
import results from './api/results.ts'
import suppliers from './api/suppliers.ts'
export const FinancialYear = Type.Object({
year: Type.Number(),
@ -26,6 +27,7 @@ const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
fastify.register(journals, { prefix: '/journals' })
fastify.register(objects, { prefix: '/objects' })
fastify.register(results, { prefix: '/results' })
fastify.register(suppliers, { prefix: '/suppliers' })
done()
}

View File

@ -20,7 +20,38 @@ const entryRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
return null
}
return knex('entry').select('*').orderBy('entryDate').where({ financialYearId, journalId })
return knex('entry AS e')
.select('e.*')
.sum('t.amount AS amount')
.innerJoin('transaction AS t', 'e.id', 't.entry_id')
.orderBy('e.id')
.where({ financialYearId, journalId })
.andWhere('t.amount', '>', 0)
.groupBy('e.id')
},
})
fastify.route({
url: '/:id',
method: 'GET',
schema: {
params: Type.Object({
id: Type.Number(),
}),
},
async handler(req) {
return knex('entry AS e')
.first('e.id', 'j.identifier AS journal', 'e.number', 'e.entryDate', 'e.transactionDate', 'e.description', {
transactions: knex
.select(knex.raw('json_agg(transactions)'))
.from(knex('transaction').select('*').where('transaction.entryId', knex.ref('e.id')).as('transactions')),
})
.sum('t.amount AS amount')
.innerJoin('journal AS j', 'e.journalId', 'j.id')
.innerJoin('transaction AS t', 'e.id', 't.entry_id')
.where('e.id', req.params.id)
.andWhere('t.amount', '>', 0)
.groupBy('e.id', 'j.identifier')
},
})

View File

@ -44,10 +44,38 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
.from(knex.select('*').from('transaction AS t').where('t.invoice_id', knex.ref('i.id')).as('transactions')),
})
.leftOuterJoin('financialYear AS fy', 'i.financialYearId', 'fy.id')
.orderBy('i.invoiceDate')
.where(query)
},
})
fastify.route({
url: '/total-amount',
method: 'GET',
schema: {
querystring: Type.Object({
year: Type.Optional(Type.Number()),
supplier: Type.Optional(Type.Number()),
}),
},
async handler(req) {
let query: { financialYearId?: number; supplierId?: number } = {}
if (req.query.year) {
const year = await knex('financialYear').first('*').where('year', req.query.year)
if (!year) throw new StatusError(404, `Year ${req.query.year} not found.`)
query.financialYearId = year.id
}
if (req.query.supplier) {
query.supplierId = req.query.supplier
}
return knex('invoice AS i').first().sum('i.amount AS amount').where(query)
},
})
fastify.route({
url: '/:id',
method: 'GET',

View File

@ -7,36 +7,62 @@ const resultRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/',
method: 'GET',
async handler() {
const years = await knex('financialYear').select('*')
const financialYears = await knex('financialYear').select('*').orderBy('year', 'asc')
const accounts = await knex('account').select('*')
return Promise.all(
years.map((year) =>
knex('account AS a')
.select('a.number', 'a.description')
.sum('t.amount as amount')
.innerJoin('transaction AS t', function () {
this.on('t.accountNumber', '=', 'a.number')
})
.innerJoin('entry AS e', function () {
this.on('t.entryId', '=', 'e.id')
})
.groupBy('a.number', 'a.description')
.where('a.number', '>=', 3000)
.where('e.financialYearId', year.id)
.orderBy('a.number')
.then((result) => ({
startDate: year.startDate,
endDate: year.endDate,
result,
})),
),
).then((years) => ({
accounts,
years,
}))
return knex('transaction AS t')
.select(
't.accountNumber',
'a.description',
Object.fromEntries(
financialYears.map((fy) => [
fy.year,
knex.raw(`SUM(CASE WHEN fy.year = ${fy.year} THEN t.amount ELSE 0 END)`),
]),
),
)
.sum('t.amount AS amount')
.innerJoin('entry AS e', function () {
this.on('t.entryId', '=', 'e.id')
})
.innerJoin('financialYear AS fy', 'fy.id', 'e.financialYearId')
.innerJoin('account AS a', function () {
this.on('t.accountNumber', '=', 'a.number')
})
.groupBy('t.accountNumber', 'a.description')
.where('t.accountNumber', '>=', 3000)
.orderBy('t.accountNumber')
},
// async handler() {
// const years = await knex('financialYear').select('*')
// const accounts = await knex('account').select('*')
// return Promise.all(
// years.map((year) =>
// knex('account AS a')
// .select('a.number', 'a.description')
// .sum('t.amount as amount')
// .innerJoin('transaction AS t', function () {
// this.on('t.accountNumber', '=', 'a.number')
// })
// .innerJoin('entry AS e', function () {
// this.on('t.entryId', '=', 'e.id')
// })
// .groupBy('a.number', 'a.description')
// .where('a.number', '>=', 3000)
// .where('e.financialYearId', year.id)
// .orderBy('a.number')
// .then((result) => ({
// startDate: year.startDate,
// endDate: year.endDate,
// result,
// })),
// ),
// ).then((years) => ({
// accounts,
// years,
// }))
// },
})
fastify.route({

View File

@ -24,6 +24,31 @@ const journalRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
},
})
fastify.route({
url: '/merge',
method: 'POST',
schema: {
body: Type.Object({
ids: Type.Array(Type.Number()),
}),
},
async handler(req) {
console.dir(req.body)
const suppliers = await knex('supplier').select('*').whereIn('id', req.body.ids)
const trx = await knex.transaction()
await trx('invoice').update('supplier_id', req.body.ids[0]).whereIn('supplierId', req.body.ids.slice(1))
await trx('supplier').delete().whereIn('id', req.body.ids.slice(1))
// 556744-4301
trx.commit()
return suppliers
},
})
done()
}