results and other stuffs
This commit is contained in:
parent
723d8840c8
commit
fa4563f771
20
.bruno/BRF/api-entries--id.bru
Normal file
20
.bruno/BRF/api-entries--id.bru
Normal 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
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/financial-years
|
||||
type: http
|
||||
seq: 4
|
||||
seq: 5
|
||||
}
|
||||
|
||||
get {
|
||||
|
||||
@ -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
|
||||
|
||||
15
.bruno/BRF/api-invoices-total-amount.bru
Normal file
15
.bruno/BRF/api-invoices-total-amount.bru
Normal 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
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/invoices
|
||||
type: http
|
||||
seq: 7
|
||||
seq: 8
|
||||
}
|
||||
|
||||
get {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/objects/:id
|
||||
type: http
|
||||
seq: 5
|
||||
seq: 6
|
||||
}
|
||||
|
||||
get {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/objects
|
||||
type: http
|
||||
seq: 6
|
||||
seq: 7
|
||||
}
|
||||
|
||||
get {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/results/:year
|
||||
type: http
|
||||
seq: 11
|
||||
seq: 12
|
||||
}
|
||||
|
||||
get {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/results
|
||||
type: http
|
||||
seq: 10
|
||||
seq: 11
|
||||
}
|
||||
|
||||
get {
|
||||
|
||||
15
.bruno/BRF/api-suppliers-merge.bru
Normal file
15
.bruno/BRF/api-suppliers-merge.bru
Normal 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
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/suppliers
|
||||
type: http
|
||||
seq: 9
|
||||
seq: 10
|
||||
}
|
||||
|
||||
get {
|
||||
|
||||
101
client/public/components/entries_page.tsx
Normal file
101
client/public/components/entries_page.tsx
Normal 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
|
||||
92
client/public/components/entry_page.tsx
Normal file
92
client/public/components/entry_page.tsx
Normal 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
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
{years.length && results.length && (
|
||||
<table className={s.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account</th>
|
||||
<th>Description</th>
|
||||
{years.map((year) => (
|
||||
<th>{year}</th>
|
||||
))}
|
||||
</div>
|
||||
{currentYear ? (
|
||||
<div>
|
||||
<h2>{currentYear}</h2>
|
||||
<Result year={currentYear} />
|
||||
</div>
|
||||
) : null}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
3
client/public/components/select.tsx
Normal file
3
client/public/components/select.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import selectFactory from '../../shared/components/select_factory.tsx'
|
||||
|
||||
export default selectFactory({ styles: {} })
|
||||
@ -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',
|
||||
|
||||
@ -3,3 +3,8 @@
|
||||
*:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
16
pnpm-lock.yaml
generated
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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')
|
||||
})
|
||||
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')
|
||||
})
|
||||
.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,
|
||||
}))
|
||||
.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({
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user