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 { meta {
name: /api/financial-years name: /api/financial-years
type: http type: http
seq: 4 seq: 5
} }
get { get {

View File

@ -5,11 +5,15 @@ meta {
} }
get { get {
url: {{base_url}}/api/invoices/2631 url: {{base_url}}/api/invoices/:id
body: none body: none
auth: inherit auth: inherit
} }
params:path {
id: 1000
}
settings { settings {
encodeUrl: true encodeUrl: true
timeout: 0 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 { meta {
name: /api/invoices name: /api/invoices
type: http type: http
seq: 7 seq: 8
} }
get { get {

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/results name: /api/results
type: http type: http
seq: 10 seq: 11
} }
get { 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 { meta {
name: /api/suppliers name: /api/suppliers
type: http type: http
seq: 9 seq: 10
} }
get { 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> <table>
<thead> <thead>
<tr> <tr>
<th>Entry</th>
<th>Account</th> <th>Account</th>
<th>Debit</th> <th>Debit</th>
<th>Credit</th> <th>Credit</th>
@ -87,6 +88,9 @@ const InvoicePage = () => {
<tbody> <tbody>
{invoice?.transactions?.map((transaction) => ( {invoice?.transactions?.map((transaction) => (
<tr> <tr>
<td>
<a href={`/entries/${transaction.entry_id}`}>{transaction.entry_id}</a>
</td>
<td>{transaction.account_number}</td> <td>{transaction.account_number}</td>
<td>{transaction.amount >= 0 ? formatNumber(transaction.amount) : null}</td> <td>{transaction.amount >= 0 ? formatNumber(transaction.amount) : null}</td>
<td>{transaction.amount < 0 ? formatNumber(Math.abs(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 InvoicesPage = () => {
const [supplier, setSupplier] = useState(null) const [supplier, setSupplier] = useState(null)
const [invoices, setInvoices] = useState([]) const [invoices, setInvoices] = useState([])
const [totalAmount, setTotalAmount] = useState<number>(null)
const route = useRoute() const route = useRoute()
useEffect(() => { useEffect(() => {
rek(`/api/suppliers/${route.params.supplier}`).then((supplier) => setSupplier(supplier)) rek(`/api/suppliers/${route.params.supplier}`).then((supplier) => setSupplier(supplier))
rek(`/api/invoices?supplier=${route.params.supplier}`).then((invoices) => setInvoices(invoices)) 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]) }, [route.params.supplier])
return ( return (
@ -26,6 +30,10 @@ const InvoicesPage = () => {
<h1>Invoices for {supplier?.name}</h1> <h1>Invoices for {supplier?.name}</h1>
<p>
<strong>Total: {totalAmount}</strong>
</p>
<table> <table>
<thead> <thead>
<tr> <tr>

View File

@ -28,7 +28,9 @@ const InvoicesPage = () => {
<ul> <ul>
{suppliers?.map((supplier) => ( {suppliers?.map((supplier) => (
<li> <li>
<a href={`/invoices/by-supplier/${supplier.id}`}>{supplier.name}</a> <a href={`/invoices/by-supplier/${supplier.id}`}>
({supplier.id}) {supplier.name}
</a>
</li> </li>
))} ))}
</ul> </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 { .table {
display: flex; td:nth-child(3),
flex-direction: row-reverse; td:nth-child(3) ~ td {
justify-content: start; 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 rek from 'rek'
import Head from './head.ts' import Head from './head.ts'
import Result from './result.tsx' import Result from './result.tsx'
import Results from './results.tsx'
import { formatNumber } from '../utils/format_number.ts'
import s from './results_page.module.scss' import s from './results_page.module.scss'
const ResultsPage = () => { const ResultsPage = () => {
const [financialYears, setFinancialYears] = useState([]) const [results, setResults] = useState([])
const [currentYear, setCurrentYear] = useState<number>(null) const [years, setYears] = useState<number[]>([])
useEffect(() => { useEffect(() => {
rek('/api/financial-years').then((financialYears) => { rek(`/api/results`).then(setResults)
setFinancialYears(financialYears) rek(`/api/financial-years`).then((years) => setYears(years.map((fy) => fy.year).toReversed()))
setCurrentYear(financialYears[financialYears.length - 1].year)
})
}, []) }, [])
return ( return (
@ -23,17 +23,30 @@ const ResultsPage = () => {
</Head> </Head>
<h1>Results</h1> <h1>Results</h1>
<div className={s.years}> {years.length && results.length && (
{financialYears.map((financialYear) => ( <table className={s.table}>
<button onClick={() => setCurrentYear(financialYear.year)}>{financialYear.year}</button> <thead>
))} <tr>
</div> <th>Account</th>
{currentYear ? ( <th>Description</th>
<div> {years.map((year) => (
<h2>{currentYear}</h2> <th>{year}</th>
<Result year={currentYear} /> ))}
</div> </tr>
) : null} </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> </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 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 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'
@ -19,6 +21,19 @@ export default [
title: 'Accounts', title: 'Accounts',
component: Accounts, component: Accounts,
}, },
{
path: '/entries',
name: 'entries',
title: 'Entries',
component: Entries,
},
{
path: '/entries/:id',
name: 'entry',
title: 'Entry',
component: Entry,
nav: false,
},
{ {
path: '/objects', path: '/objects',
name: 'objects', name: 'objects',

View File

@ -3,3 +3,8 @@
*:after { *:after {
box-sizing: border-box; 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', { const numberFormatter = new Intl.NumberFormat('sv-SE', {
style: 'decimal', style: 'decimal',
minimumFractionDigits: 0, minimumFractionDigits: 2,
maximumFractionDigits: 0, maximumFractionDigits: 2,
}) })
export const formatNumber = (nbr) => numberFormatter.format(nbr) 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 [touched, setTouched] = useState(false)
const selectRef = useRef<HTMLSelectElement>() const selectRef = useRef<HTMLSelectElement>()
console.log(options)
const onBlur = useCallback(() => setTouched(true), []) const onBlur = useCallback(() => setTouched(true), [])
useEffect(() => { useEffect(() => {

View File

@ -2,7 +2,7 @@
-- PostgreSQL database dump -- PostgreSQL database dump
-- --
\restrict LKaLyA1IGSZwb31KR2e5GrFZPB7KuPkTsiIxHVwV0aqqukXTaBdKHj8rPqgoMsg \restrict FugYqvehfvYcZV6n0VXYfKK3pEfWehcjXHsTSddhC5Qcn0530oCENplg6a2CdZd
-- 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
@ -89,6 +89,37 @@ CREATE SEQUENCE public.account_id_seq
ALTER SEQUENCE public.account_id_seq OWNED BY public.account.id; 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: - -- 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 ( CREATE TABLE public.supplier (
id integer NOT NULL, id integer NOT NULL,
name text, 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); 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: - -- Name: dimension id; Type: DEFAULT; Schema: public; Owner: -
-- --
@ -539,6 +578,22 @@ ALTER TABLE ONLY public.account
ADD CONSTRAINT account_pkey PRIMARY KEY (id); 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: - -- 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); 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: - -- 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); 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: - -- 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 -- PostgreSQL database dump complete
-- --
\unrestrict LKaLyA1IGSZwb31KR2e5GrFZPB7KuPkTsiIxHVwV0aqqukXTaBdKHj8rPqgoMsg \unrestrict FugYqvehfvYcZV6n0VXYfKK3pEfWehcjXHsTSddhC5Qcn0530oCENplg6a2CdZd

View File

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

16
pnpm-lock.yaml generated
View File

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

View File

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

View File

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

View File

@ -20,7 +20,38 @@ const entryRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
return null 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')), .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') .leftOuterJoin('financialYear AS fy', 'i.financialYearId', 'fy.id')
.orderBy('i.invoiceDate')
.where(query) .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({ fastify.route({
url: '/:id', url: '/:id',
method: 'GET', method: 'GET',

View File

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