Use clusterize.js for general ledger view to avoid pagination
This commit is contained in:
parent
ace8629bc3
commit
eb72c3be95
@ -14,11 +14,13 @@
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-shell": "^2",
|
||||
"@tauri-apps/plugin-sql": "~2",
|
||||
"clusterize.js": "^1.0.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/clusterize.js": "^0.18.3",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
|
@ -20,6 +20,9 @@ importers:
|
||||
'@tauri-apps/plugin-sql':
|
||||
specifier: ~2
|
||||
version: 2.0.1
|
||||
clusterize.js:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
vue:
|
||||
specifier: ^3.3.4
|
||||
version: 3.5.12(typescript@5.6.3)
|
||||
@ -30,6 +33,9 @@ importers:
|
||||
'@tauri-apps/cli':
|
||||
specifier: ^2
|
||||
version: 2.1.0
|
||||
'@types/clusterize.js':
|
||||
specifier: ^0.18.3
|
||||
version: 0.18.3
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^5.0.5
|
||||
version: 5.2.0(vite@5.4.11)(vue@3.5.12(typescript@5.6.3))
|
||||
@ -418,6 +424,9 @@ packages:
|
||||
'@tauri-apps/plugin-sql@2.0.1':
|
||||
resolution: {integrity: sha512-SxvRO/qwq/dHHGJ+79Bx4tB/wlfUE44sP1+wpuGOp11fgmfmOaf3nlZAl0P0KX+U3h0rwR/f7PMRQ6Eg408DYQ==}
|
||||
|
||||
'@types/clusterize.js@0.18.3':
|
||||
resolution: {integrity: sha512-udptC3aq8hfaXgmt9lC73OuE4RJYt26D2XIj+fTNDs0wuzAgQ6cyDpQOSkWhU65NroISAWhZ3/aovvV88IX7Gw==}
|
||||
|
||||
'@types/estree@1.0.6':
|
||||
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
||||
|
||||
@ -546,6 +555,9 @@ packages:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
clusterize.js@1.0.0:
|
||||
resolution: {integrity: sha512-EEYhO8rOvw9JVaHLgEFdvvg9H6ug/GVl8KgakOoc9hg4FK6xmyYsC4B0Aw/QI6ClPxaGPKBetO+ISvCY8N/uUQ==}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@ -1236,6 +1248,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
'@types/clusterize.js@0.18.3': {}
|
||||
|
||||
'@types/estree@1.0.6': {}
|
||||
|
||||
'@vitejs/plugin-vue@5.2.0(vite@5.4.11)(vue@3.5.12(typescript@5.6.3))':
|
||||
@ -1395,6 +1409,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
clusterize.js@1.0.0: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
@ -22,52 +22,39 @@
|
||||
</h1>
|
||||
|
||||
<div class="my-4 flex">
|
||||
<button v-if="commodityDetail" class="btn-secondary" @click="commodityDetail = false">Hide commodity detail</button>
|
||||
<button v-if="!commodityDetail" class="btn-secondary" @click="commodityDetail = true">Show commodity detail</button>
|
||||
<button v-if="commodityDetail" class="btn-secondary" @click="commodityDetail = false; renderTable();">Hide commodity detail</button>
|
||||
<button v-if="!commodityDetail" class="btn-secondary" @click="commodityDetail = true; renderTable();">Show commodity detail</button>
|
||||
</div>
|
||||
|
||||
<table class="min-w-full" ref="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="py-0.5 pr-1 text-gray-900 font-semibold text-start">Date</th>
|
||||
<th class="py-0.5 px-1 text-gray-900 font-semibold text-start" colspan="3">Description</th>
|
||||
<th class="py-0.5 px-1 text-gray-900 font-semibold text-end">Dr</th>
|
||||
<th class="py-0.5 pl-1 text-gray-900 font-semibold text-end">Cr</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="transaction-list">
|
||||
<template v-for="transaction in transactions" :key="transaction.id">
|
||||
<tr class="border-t border-gray-300">
|
||||
<td class="py-0.5 pr-1 text-gray-900">{{ transaction.dt.split(' ')[0] }}</td>
|
||||
<td class="py-0.5 px-1 text-gray-900" colspan="3">{{ transaction.description }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<div id="transaction-list" class="max-h-[100vh] overflow-y-scroll wk-aa">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="py-0.5 pr-1 text-gray-900 font-semibold lg:w-[12ex] text-start">Date</th>
|
||||
<th class="py-0.5 px-1 text-gray-900 font-semibold text-start" colspan="3">Description</th>
|
||||
<template v-if="commodityDetail">
|
||||
<th class="py-0.5 px-1 text-gray-900 font-semibold text-end">Dr</th>
|
||||
<th class="py-0.5 pl-1 text-gray-900 font-semibold text-end">Cr</th>
|
||||
</template>
|
||||
<template v-if="!commodityDetail">
|
||||
<th class="py-0.5 px-1 text-gray-900 font-semibold lg:w-[12ex] text-end">Dr</th>
|
||||
<th class="py-0.5 pl-1 text-gray-900 font-semibold lg:w-[12ex] text-end">Cr</th>
|
||||
</template>
|
||||
</tr>
|
||||
<template v-for="posting in transaction.postings" :key="posting.id">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="py-0.5 px-1 text-gray-900">{{ posting.description }}</td>
|
||||
<td class="py-0.5 px-1 text-gray-900 text-end"><i>{{ posting.quantity >= 0 ? 'Dr' : 'Cr' }}</i></td>
|
||||
<td class="py-0.5 px-1 text-gray-900">{{ posting.account }}</td>
|
||||
<td class="py-0.5 px-1 text-gray-900 text-end">
|
||||
{{ posting.quantity >= 0 ? (commodityDetail ? ppWithCommodity(posting.quantity, posting.commodity) : pp(asCost(posting.quantity, posting.commodity))) : '' }}
|
||||
</td>
|
||||
<td class="py-0.5 pl-1 text-gray-900 text-end">
|
||||
{{ posting.quantity < 0 ? (commodityDetail ? ppWithCommodity(-posting.quantity, posting.commodity) : pp(asCost(-posting.quantity, posting.commodity))) : '' }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="my-4 flex" v-if="transactionsOffset !== null">
|
||||
<button class="btn-secondary" @click="load()">Load more…</button>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="4">Loading data…</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
|
||||
import Clusterize from 'clusterize.js';
|
||||
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
|
||||
import { asCost } from './commodities.ts';
|
||||
import { db } from './db.ts';
|
||||
@ -90,29 +77,18 @@
|
||||
commodity: string
|
||||
}
|
||||
|
||||
const transactions = ref([] as _Transaction[]);
|
||||
const transactionsOffset = ref(0 as number | null);
|
||||
const transactions: _Transaction[] = [];
|
||||
let clusterize: Clusterize | null = null;
|
||||
|
||||
async function load() {
|
||||
if (transactionsOffset.value === null) {
|
||||
// No more entries
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await db.load();
|
||||
|
||||
const transactionsRaw: {transaction_id: number, dt: string, transaction_description: string, id: number, description: string, account: string, quantity: number, commodity: string}[] = await session.select('SELECT transaction_id, dt, transactions.description AS transaction_description, postings.id, postings.description, account, quantity, commodity FROM transactions LEFT JOIN postings ON transactions.id = postings.transaction_id ORDER BY dt DESC, transaction_id DESC, postings.id LIMIT 200 OFFSET ?', [transactionsOffset.value]);
|
||||
|
||||
if (transactionsRaw.length === 0) {
|
||||
// No more entries
|
||||
transactionsOffset.value = null;
|
||||
return;
|
||||
}
|
||||
const transactionsRaw: {transaction_id: number, dt: string, transaction_description: string, id: number, description: string, account: string, quantity: number, commodity: string}[] = await session.select('SELECT transaction_id, dt, transactions.description AS transaction_description, postings.id, postings.description, account, quantity, commodity FROM transactions LEFT JOIN postings ON transactions.id = postings.transaction_id ORDER BY dt DESC, transaction_id DESC, postings.id');
|
||||
|
||||
// Group postings into transactions
|
||||
for (const transactionRaw of transactionsRaw) {
|
||||
if (transactions.value.length === 0 || transactions.value.at(-1)!.id !== transactionRaw.transaction_id) {
|
||||
transactions.value.push({
|
||||
if (transactions.length === 0 || transactions.at(-1)!.id !== transactionRaw.transaction_id) {
|
||||
transactions.push({
|
||||
id: transactionRaw.transaction_id,
|
||||
dt: transactionRaw.dt,
|
||||
description: transactionRaw.transaction_description,
|
||||
@ -120,7 +96,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
transactions.value.at(-1)!.postings.push({
|
||||
transactions.at(-1)!.postings.push({
|
||||
id: transactionRaw.id,
|
||||
description: transactionRaw.description,
|
||||
account: transactionRaw.account,
|
||||
@ -129,7 +105,72 @@
|
||||
});
|
||||
}
|
||||
|
||||
transactionsOffset.value += transactionsRaw.length;
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const rows = [];
|
||||
|
||||
for (const transaction of transactions) {
|
||||
rows.push(
|
||||
`<tr class="border-t border-gray-300">
|
||||
<td class="py-0.5 pr-1 text-gray-900 lg:w-[12ex]">${ transaction.dt.split(' ')[0] }</td>
|
||||
<td class="py-0.5 px-1 text-gray-900" colspan="3">${ transaction.description }</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>`
|
||||
);
|
||||
for (const posting of transaction.postings) {
|
||||
if (commodityDetail.value) {
|
||||
rows.push(
|
||||
`<tr>
|
||||
<td class=""></td>
|
||||
<td class="py-0.5 px-1 text-gray-900 lg:w-[30%]">${ posting.description || '' }</td>
|
||||
<td class="py-0.5 px-1 text-gray-900 text-end"><i>${ posting.quantity >= 0 ? 'Dr' : 'Cr' }</i></td>
|
||||
<td class="py-0.5 px-1 text-gray-900 lg:w-[30%]">${ posting.account }</td>
|
||||
<td class="py-0.5 px-1 text-gray-900 text-end">
|
||||
${ posting.quantity >= 0 ? ppWithCommodity(posting.quantity, posting.commodity) : '' }
|
||||
</td>
|
||||
<td class="py-0.5 pl-1 text-gray-900 text-end">
|
||||
${ posting.quantity < 0 ? ppWithCommodity(-posting.quantity, posting.commodity) : '' }
|
||||
</td>
|
||||
</tr>`
|
||||
);
|
||||
} else {
|
||||
rows.push(
|
||||
`<tr>
|
||||
<td class=""></td>
|
||||
<td class="py-0.5 px-1 text-gray-900 lg:w-[30%]">${ posting.description || '' }</td>
|
||||
<td class="py-0.5 px-1 text-gray-900 text-end"><i>${ posting.quantity >= 0 ? 'Dr' : 'Cr' }</i></td>
|
||||
<td class="py-0.5 px-1 text-gray-900 lg:w-[30%]">${ posting.account }</td>
|
||||
<td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">
|
||||
${ posting.quantity >= 0 ? pp(asCost(posting.quantity, posting.commodity)) : '' }
|
||||
</td>
|
||||
<td class="py-0.5 pl-1 text-gray-900 lg:w-[12ex] text-end">
|
||||
${ posting.quantity < 0 ? pp(asCost(-posting.quantity, posting.commodity)) : '' }
|
||||
</td>
|
||||
</tr>`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clusterize === null) {
|
||||
clusterize = new Clusterize({
|
||||
'rows': rows,
|
||||
scrollElem: document.getElementById('transaction-list')!,
|
||||
contentElem: document.querySelector('#transaction-list tbody')!
|
||||
});
|
||||
} else {
|
||||
clusterize.update(rows);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
onUnmounted(() => {
|
||||
if (clusterize !== null) {
|
||||
clusterize.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -36,4 +36,8 @@
|
||||
.page-heading {
|
||||
@apply text-xl sm:text-base font-medium text-gray-700 print:text-xl print:text-gray-900;
|
||||
}
|
||||
.wk-aa {
|
||||
/* When we use overflow: scroll, WebKit automatically disables antialiasing */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user