Implement chart of accounts view
This commit is contained in:
parent
efa42415a9
commit
8ccbe44102
56
src/components/DropdownBox.vue
Normal file
56
src/components/DropdownBox.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<!--
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<button type="button" class="relative w-full cursor-default bg-white bordered-field pl-3 pr-10 text-left" @click="isOpen = !isOpen">
|
||||
<span class="block truncate">{{ selectedValue[1] }}</span>
|
||||
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" />
|
||||
</span>
|
||||
</button>
|
||||
<ul class="absolute z-20 mt-1 max-h-60 w-full overflow-auto bg-white py-1 text-sm shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" :class="isOpen ? 'block' : 'hidden'">
|
||||
<template v-for="([categoryName, categoryItems], index) in values">
|
||||
<li class="relative cursor-default select-none py-1 pl-3 pr-9 text-gray-500 border-b border-gray-300" :class="{ 'pt-4': index > 0 }">
|
||||
<span class="block truncate text-xs font-bold uppercase">{{ categoryName }}</span>
|
||||
</li>
|
||||
<li v-for="item in categoryItems" class="group relative cursor-default select-none py-1 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-emerald-600" :data-selected="item[0] === selectedValue[0] ? 'selected' : null" @click="selectedValue = item; isOpen = false">
|
||||
<span class="block truncate group-data-[selected=selected]:font-semibold">{{ item[1] }}</span>
|
||||
<span class="hidden group-data-[selected=selected]:flex absolute inset-y-0 right-0 items-center pr-4 text-emerald-600 group-hover:text-white">
|
||||
<CheckIcon class="h-5 w-5" />
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/vue/24/outline';
|
||||
|
||||
import { defineModel, defineProps, ref } from 'vue';
|
||||
|
||||
const { values } = defineProps<{ values: [string, [string, string][]][] }>(); // Array of [category name, [internal identifier, pretty name]]
|
||||
|
||||
const selectedValue = defineModel({ default: null! as [string, string] }); // Vue bug: Compiler produces broken code if setting default directly here
|
||||
if (selectedValue.value === null) {
|
||||
selectedValue.value = values[0][1][0];
|
||||
}
|
||||
|
||||
const isOpen = ref(false);
|
||||
</script>
|
@ -30,6 +30,7 @@ async function initApp() {
|
||||
// Init router
|
||||
const routes = [
|
||||
{ path: '/', name: 'index', component: () => import('./pages/HomeView.vue') },
|
||||
{ path: '/chart-of-accounts', name: 'chart-of-accounts', component: () => import('./pages/ChartOfAccountsView.vue') },
|
||||
{ path: '/general-ledger', name: 'general-ledger', component: () => import('./pages/GeneralLedgerView.vue') },
|
||||
{ path: '/journal', name: 'journal', component: () => import('./pages/JournalView.vue') },
|
||||
{ path: '/journal/edit-transaction/:id', name: 'journal-edit-transaction', component: () => import('./pages/EditTransactionView.vue') },
|
||||
|
132
src/pages/ChartOfAccountsView.vue
Normal file
132
src/pages/ChartOfAccountsView.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<!--
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<h1 class="page-heading">
|
||||
Chart of accounts
|
||||
</h1>
|
||||
|
||||
<div class="my-2 py-2 flex gap-x-2 items-baseline bg-white sticky top-0">
|
||||
<DropdownBox class="w-[450px]" :values="accountKindsByModule" v-model="selectedAccountKind" />
|
||||
<button class="btn-primary" @click="addAccountType">Add type</button>
|
||||
<button class="btn-secondary text-red-600 ring-red-500" @click="removeAccountType">Remove type</button>
|
||||
</div>
|
||||
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="py-0.5 px-1 text-gray-900 font-semibold text-start">Account</th>
|
||||
<th class="py-0.5 pl-1 text-gray-900 font-semibold text-start">Associated types</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="border-t border-gray-300" v-for="[account, thisAccountKinds] in accounts.entries()">
|
||||
<td class="py-0.5 pr-1 text-gray-900 align-baseline"><input class="checkbox-primary" type="checkbox" v-model="selectedAccounts" :value="account"></td>
|
||||
<td class="py-0.5 px-1 text-gray-900 align-baseline">{{ account }}</td>
|
||||
<td class="py-0.5 pl-1 text-gray-900 align-baseline">
|
||||
<ul class="list-disc ml-5" v-if="thisAccountKinds">
|
||||
<!-- First display known account kinds -->
|
||||
<template v-for="[accountKind, accountKindPrettyName] in accountKindsMap.entries()">
|
||||
<li v-if="thisAccountKinds.indexOf(accountKind) >= 0">{{ accountKindPrettyName }}</li>
|
||||
</template>
|
||||
<!-- Then display unknown account kinds -->
|
||||
<template v-for="accountKind in thisAccountKinds">
|
||||
<li v-if="!accountKindsMap.has(accountKind)" class="italic">{{ accountKind }}</li>
|
||||
</template>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { accountKinds } from '../registry.ts';
|
||||
import { db } from '../db.ts';
|
||||
import DropdownBox from '../components/DropdownBox.vue';
|
||||
|
||||
const accountKindsMap = new Map(accountKinds);
|
||||
const accountKindsByModule = [...Map.groupBy(accountKinds, (k) => k[0].split('.')[0]).entries()];
|
||||
|
||||
const accounts = ref(new Map<string, string[]>());
|
||||
const selectedAccounts = ref([]);
|
||||
const selectedAccountKind = ref(accountKinds[0]);
|
||||
|
||||
async function load() {
|
||||
const session = await db.load();
|
||||
|
||||
const accountKindsRaw: {account: string, kind: string}[] = await session.select(
|
||||
`SELECT account, kind FROM account_configurations ORDER BY account, kind`
|
||||
);
|
||||
|
||||
for (const accountKindRaw of accountKindsRaw) {
|
||||
const kinds = accounts.value.get(accountKindRaw.account) ?? [];
|
||||
kinds.push(accountKindRaw.kind);
|
||||
accounts.value.set(accountKindRaw.account, kinds);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
async function addAccountType() {
|
||||
// Associate selected accounts with the selected account kind
|
||||
const session = await db.load();
|
||||
const dbTransaction = await session.begin();
|
||||
|
||||
for (const account of selectedAccounts.value) {
|
||||
await dbTransaction.execute(
|
||||
`INSERT INTO account_configurations (account, kind)
|
||||
VALUES ($1, $2)`,
|
||||
[account, selectedAccountKind.value[0]]
|
||||
);
|
||||
}
|
||||
|
||||
await dbTransaction.commit();
|
||||
|
||||
selectedAccounts.value = [];
|
||||
|
||||
// Reload data
|
||||
accounts.value.clear();
|
||||
await load();
|
||||
}
|
||||
|
||||
async function removeAccountType() {
|
||||
// De-associate selected accounts with the selected account kind
|
||||
const session = await db.load();
|
||||
const dbTransaction = await session.begin();
|
||||
|
||||
for (const account of selectedAccounts.value) {
|
||||
await dbTransaction.execute(
|
||||
`DELETE FROM account_configurations
|
||||
WHERE account = $1 AND kind = $2`,
|
||||
[account, selectedAccountKind.value[0]]
|
||||
);
|
||||
}
|
||||
|
||||
await dbTransaction.commit();
|
||||
|
||||
selectedAccounts.value = [];
|
||||
|
||||
// Reload data
|
||||
accounts.value.clear();
|
||||
await load();
|
||||
}
|
||||
</script>
|
@ -24,7 +24,7 @@
|
||||
<li><RouterLink :to="{ name: 'journal' }" class="text-gray-900 hover:text-blue-700 hover:underline">Journal</RouterLink></li>
|
||||
<li><RouterLink :to="{ name: 'statement-lines' }" class="text-gray-900 hover:text-blue-700 hover:underline">Statement lines</RouterLink></li>
|
||||
<!--<li><a href="#" class="text-gray-900 hover:text-blue-700 hover:underline">Balance assertions</a></li>-->
|
||||
<!--<li><a href="#" class="text-gray-900 hover:text-blue-700 hover:underline">Chart of accounts</a></li>-->
|
||||
<li><RouterLink :to="{ name: 'chart-of-accounts' }" class="text-gray-900 hover:text-blue-700 hover:underline">Chart of accounts</RouterLink></li>
|
||||
<!-- TODO: Plugin reports -->
|
||||
</ul>
|
||||
</div>
|
||||
|
25
src/registry.ts
Normal file
25
src/registry.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const accountKinds: [string, string][] = [
|
||||
['drcr.asset', 'Asset'],
|
||||
['drcr.liability', 'Liability'],
|
||||
['drcr.income', 'Income'],
|
||||
['drcr.expense', 'Expense'],
|
||||
['drcr.equity', 'Equity']
|
||||
];
|
@ -1,9 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
|
Loading…
Reference in New Issue
Block a user