diff --git a/.bruno/BRF/api-entries--id.bru b/.bruno/BRF/api-entries--id.bru new file mode 100644 index 0000000..89aa559 --- /dev/null +++ b/.bruno/BRF/api-entries--id.bru @@ -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 +} diff --git a/.bruno/BRF/api-financial-years.bru b/.bruno/BRF/api-financial-years.bru index 991dd5b..116a858 100644 --- a/.bruno/BRF/api-financial-years.bru +++ b/.bruno/BRF/api-financial-years.bru @@ -1,7 +1,7 @@ meta { name: /api/financial-years type: http - seq: 4 + seq: 5 } get { diff --git a/.bruno/BRF/api-invoices--id.bru b/.bruno/BRF/api-invoices--id.bru index 4890c9c..e17cad0 100644 --- a/.bruno/BRF/api-invoices--id.bru +++ b/.bruno/BRF/api-invoices--id.bru @@ -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 diff --git a/.bruno/BRF/api-invoices-total-amount.bru b/.bruno/BRF/api-invoices-total-amount.bru new file mode 100644 index 0000000..c6dd80b --- /dev/null +++ b/.bruno/BRF/api-invoices-total-amount.bru @@ -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 +} diff --git a/.bruno/BRF/api-invoices.bru b/.bruno/BRF/api-invoices.bru index 5d020b2..6e8aa2b 100644 --- a/.bruno/BRF/api-invoices.bru +++ b/.bruno/BRF/api-invoices.bru @@ -1,7 +1,7 @@ meta { name: /api/invoices type: http - seq: 7 + seq: 8 } get { diff --git a/.bruno/BRF/api-objects--id.bru b/.bruno/BRF/api-objects--id.bru index 8bbd0eb..d37c795 100644 --- a/.bruno/BRF/api-objects--id.bru +++ b/.bruno/BRF/api-objects--id.bru @@ -1,7 +1,7 @@ meta { name: /api/objects/:id type: http - seq: 5 + seq: 6 } get { diff --git a/.bruno/BRF/api-objects.bru b/.bruno/BRF/api-objects.bru index 5e1900b..0d055d2 100644 --- a/.bruno/BRF/api-objects.bru +++ b/.bruno/BRF/api-objects.bru @@ -1,7 +1,7 @@ meta { name: /api/objects type: http - seq: 6 + seq: 7 } get { diff --git a/.bruno/BRF/api-results--year.bru b/.bruno/BRF/api-results--year.bru index 5561b81..3d6d0fa 100644 --- a/.bruno/BRF/api-results--year.bru +++ b/.bruno/BRF/api-results--year.bru @@ -1,7 +1,7 @@ meta { name: /api/results/:year type: http - seq: 11 + seq: 12 } get { diff --git a/.bruno/BRF/api-results.bru b/.bruno/BRF/api-results.bru index 652bc37..8aeb3f9 100644 --- a/.bruno/BRF/api-results.bru +++ b/.bruno/BRF/api-results.bru @@ -1,7 +1,7 @@ meta { name: /api/results type: http - seq: 10 + seq: 11 } get { diff --git a/.bruno/BRF/api-suppliers-merge.bru b/.bruno/BRF/api-suppliers-merge.bru new file mode 100644 index 0000000..d27c960 --- /dev/null +++ b/.bruno/BRF/api-suppliers-merge.bru @@ -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 +} diff --git a/.bruno/BRF/api-suppliers.bru b/.bruno/BRF/api-suppliers.bru index 222eb98..b637fb1 100644 --- a/.bruno/BRF/api-suppliers.bru +++ b/.bruno/BRF/api-suppliers.bru @@ -1,7 +1,7 @@ meta { name: /api/suppliers type: http - seq: 9 + seq: 10 } get { diff --git a/client/public/components/entries_page.tsx b/client/public/components/entries_page.tsx new file mode 100644 index 0000000..f980a1e --- /dev/null +++ b/client/public/components/entries_page.tsx @@ -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 ? ( +
+ + : Entries + +

Entries

+ +
+ + + +
+ +

+ {selectedJournal} : {selectedYear} +

+ + + + + + + + + + + + + {entries?.map((entry) => ( + + + + + + + + + ))} + +
IDNumberEntry DateTransaction DateAmountDescription
+ {entry.id} + {entry.number}{entry.entryDate?.slice(0, 10)}{entry.transactionDate?.slice(0, 10)}{entry.amount}{entry.description}
+
+ ) : null +} + +export default EntriesPage diff --git a/client/public/components/entry_page.tsx b/client/public/components/entry_page.tsx new file mode 100644 index 0000000..b41e83c --- /dev/null +++ b/client/public/components/entry_page.tsx @@ -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 ( +
+ + + {' '} + : Entry {entry.journal} {entry.number}{' '} + + +

+ Entry {entry.journal} {entry.number} +

+ + + + + + + + + + + + + + + + + + + + + + + + +
IDJournalNumberEntry DateTransaction DateAmountDescription
+ {entry.id} + {entry.journal}{entry.number}{entry.entryDate?.slice(0, 10)}{entry.transactionDate?.slice(0, 10)}{entry.amount}{entry.description}
+ +

Transactions

+ + + + + + + + + + + {entry?.transactions?.map((transaction) => ( + + + + + + + ))} + +
AccountDebitCreditDescription
{transaction.account_number}{transaction.amount >= 0 ? formatNumber(transaction.amount) : null}{transaction.amount < 0 ? formatNumber(Math.abs(transaction.amount)) : null}{transaction.description}
+
+ ) +} + +export default EntriesPage diff --git a/client/public/components/invoice_page.tsx b/client/public/components/invoice_page.tsx index 04eb88c..7c373ad 100644 --- a/client/public/components/invoice_page.tsx +++ b/client/public/components/invoice_page.tsx @@ -78,6 +78,7 @@ const InvoicePage = () => { + @@ -87,6 +88,9 @@ const InvoicePage = () => { {invoice?.transactions?.map((transaction) => ( + diff --git a/client/public/components/invoices_by_supplier_page.tsx b/client/public/components/invoices_by_supplier_page.tsx index a3e413a..2486971 100644 --- a/client/public/components/invoices_by_supplier_page.tsx +++ b/client/public/components/invoices_by_supplier_page.tsx @@ -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(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 = () => {

Invoices for {supplier?.name}

+

+ Total: {totalAmount} +

+
Entry Account Debit Credit
+ {transaction.entry_id} + {transaction.account_number} {transaction.amount >= 0 ? formatNumber(transaction.amount) : null} {transaction.amount < 0 ? formatNumber(Math.abs(transaction.amount)) : null}
diff --git a/client/public/components/invoices_page.tsx b/client/public/components/invoices_page.tsx index 2b4f64f..4f26071 100644 --- a/client/public/components/invoices_page.tsx +++ b/client/public/components/invoices_page.tsx @@ -28,7 +28,9 @@ const InvoicesPage = () => { diff --git a/client/public/components/result.tsx b/client/public/components/result.tsx deleted file mode 100644 index 4cad2eb..0000000 --- a/client/public/components/result.tsx +++ /dev/null @@ -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 = ({ year }) => { - const [result, setResults] = useState([]) - - useEffect(() => { - rek(`/api/results/${year}`).then(setResults) - }, [year]) - - return ( -
- - {result.map((result) => ( - - - - - - ))} - -
{result.accountNumber}{result.description}{result.amount}
- ) -} - -export default Result diff --git a/client/public/components/results_page.module.scss b/client/public/components/results_page.module.scss index f7918e1..014d7b6 100644 --- a/client/public/components/results_page.module.scss +++ b/client/public/components/results_page.module.scss @@ -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; + } } diff --git a/client/public/components/results_page.tsx b/client/public/components/results_page.tsx index 088ab2d..5e61506 100644 --- a/client/public/components/results_page.tsx +++ b/client/public/components/results_page.tsx @@ -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(null) + const [results, setResults] = useState([]) + const [years, setYears] = useState([]) 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 = () => {

Results

-
- {financialYears.map((financialYear) => ( - - ))} -
- {currentYear ? ( -
-

{currentYear}

- -
- ) : null} + {years.length && results.length && ( + + + + + + {years.map((year) => ( + + ))} + + + + {results.map((result) => ( + + + + {years.map((year) => ( + + ))} + + ))} + +
AccountDescription{year}
{result.accountNumber}{result.description}{formatNumber(result[year])}
+ )} ) } diff --git a/client/public/components/select.tsx b/client/public/components/select.tsx new file mode 100644 index 0000000..9124a6b --- /dev/null +++ b/client/public/components/select.tsx @@ -0,0 +1,3 @@ +import selectFactory from '../../shared/components/select_factory.tsx' + +export default selectFactory({ styles: {} }) diff --git a/client/public/routes.ts b/client/public/routes.ts index 66a5510..e96d0ef 100644 --- a/client/public/routes.ts +++ b/client/public/routes.ts @@ -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', diff --git a/client/public/styles/main.scss b/client/public/styles/main.scss index 7be123f..628963c 100644 --- a/client/public/styles/main.scss +++ b/client/public/styles/main.scss @@ -3,3 +3,8 @@ *:after { box-sizing: border-box; } + +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/client/public/utils/format_number.ts b/client/public/utils/format_number.ts index f95cdb4..8351b79 100644 --- a/client/public/utils/format_number.ts +++ b/client/public/utils/format_number.ts @@ -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) diff --git a/client/shared/components/select_factory.tsx b/client/shared/components/select_factory.tsx index a03996b..cff22cc 100644 --- a/client/shared/components/select_factory.tsx +++ b/client/shared/components/select_factory.tsx @@ -46,6 +46,8 @@ export default function selectFactory({ styles }): FunctionComponent<{ const [touched, setTouched] = useState(false) const selectRef = useRef() + console.log(options) + const onBlur = useCallback(() => setTouched(true), []) useEffect(() => { diff --git a/docker/postgres/01-schema.sql b/docker/postgres/01-schema.sql index 4b49f72..828a889 100644 --- a/docker/postgres/01-schema.sql +++ b/docker/postgres/01-schema.sql @@ -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 diff --git a/package.json b/package.json index b922284..088d93a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8331666..a06a87b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/server/lib/parse_stream.ts b/server/lib/parse_stream.ts index df41ac7..8317d6f 100644 --- a/server/lib/parse_stream.ts +++ b/server/lib/parse_stream.ts @@ -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 = {} @@ -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 } diff --git a/server/routes/api.ts b/server/routes/api.ts index e904576..b4579e5 100644 --- a/server/routes/api.ts +++ b/server/routes/api.ts @@ -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() } diff --git a/server/routes/api/entries.ts b/server/routes/api/entries.ts index 78f802a..84a895e 100644 --- a/server/routes/api/entries.ts +++ b/server/routes/api/entries.ts @@ -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') }, }) diff --git a/server/routes/api/invoices.ts b/server/routes/api/invoices.ts index 95f7c8f..a07e10f 100644 --- a/server/routes/api/invoices.ts +++ b/server/routes/api/invoices.ts @@ -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', diff --git a/server/routes/api/results.ts b/server/routes/api/results.ts index cc26626..49f61ed 100644 --- a/server/routes/api/results.ts +++ b/server/routes/api/results.ts @@ -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({ diff --git a/server/routes/api/suppliers.ts b/server/routes/api/suppliers.ts index 4ce45ba..fa9f1fa 100644 --- a/server/routes/api/suppliers.ts +++ b/server/routes/api/suppliers.ts @@ -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() }