forked from Kispi/Core
feat(core): add company bank accounts
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
5aea42d33a
commit
a2d4d89776
5 changed files with 178 additions and 45 deletions
|
@ -300,6 +300,18 @@
|
||||||
"type": "auth",
|
"type": "auth",
|
||||||
"system": false,
|
"system": false,
|
||||||
"schema": [
|
"schema": [
|
||||||
|
{
|
||||||
|
"id": "h5ogbj93",
|
||||||
|
"name": "accountNumber",
|
||||||
|
"type": "text",
|
||||||
|
"system": false,
|
||||||
|
"required": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "cvcgnf4x",
|
"id": "cvcgnf4x",
|
||||||
"name": "name",
|
"name": "name",
|
||||||
|
@ -329,5 +341,57 @@
|
||||||
"onlyEmailDomains": null,
|
"onlyEmailDomains": null,
|
||||||
"requireEmail": false
|
"requireEmail": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5msnfxat1sc2c1r",
|
||||||
|
"name": "companyTransactions",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"id": "7rnyupog",
|
||||||
|
"name": "account",
|
||||||
|
"type": "relation",
|
||||||
|
"system": false,
|
||||||
|
"required": true,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "s854d2w72fvyl54",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ruby9fp9",
|
||||||
|
"name": "label",
|
||||||
|
"type": "text",
|
||||||
|
"system": false,
|
||||||
|
"required": true,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4hiu46ib",
|
||||||
|
"name": "amount",
|
||||||
|
"type": "number",
|
||||||
|
"system": false,
|
||||||
|
"required": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": "",
|
||||||
|
"viewRule": "",
|
||||||
|
"createRule": "",
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {}
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -1,11 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="account"
|
v-if="account || company"
|
||||||
class="hero-content flex-col pt-0 text-center lg:p-6"
|
class="hero-content flex-col pt-0 text-center lg:p-6"
|
||||||
>
|
>
|
||||||
<h1 class="mt-5 text-8xl">{{ CurrencyService.toString(getBalance()) }}</h1>
|
<h1 class="mt-5 text-8xl">{{ CurrencyService.toString(getBalance()) }}</h1>
|
||||||
<h3 class="mt-10 text-5xl font-bold">Kontoinhaber: {{ `${account.firstName} ${account.lastName}` }}</h3>
|
<h3
|
||||||
<h4 class="mb-10 text-4xl">Kontonummer: {{ account.accountNumber }}</h4>
|
v-if="account"
|
||||||
|
class="mt-10 text-5xl font-bold"
|
||||||
|
>
|
||||||
|
Kontoinhaber: {{ `${account.firstName} ${account.lastName}` }}
|
||||||
|
</h3>
|
||||||
|
<h3
|
||||||
|
v-if="company"
|
||||||
|
class="mt-10 text-5xl font-bold"
|
||||||
|
>
|
||||||
|
Firma: {{ company.name }}
|
||||||
|
</h3>
|
||||||
|
<h4 class="mb-10 text-4xl">Kontonummer: {{ (account ? account : company ? company : null)?.accountNumber }}</h4>
|
||||||
<MoleculeTransactionTable
|
<MoleculeTransactionTable
|
||||||
v-if="transactions"
|
v-if="transactions"
|
||||||
class="mb-24 max-w-[42rem]"
|
class="mb-24 max-w-[42rem]"
|
||||||
|
@ -14,7 +25,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="account"
|
v-if="account || company"
|
||||||
class="fixed bottom-0 flex w-full place-content-center gap-16 bg-base-100 p-5 lg:w-[calc(100%-16rem)] lg:gap-32"
|
class="fixed bottom-0 flex w-full place-content-center gap-16 bg-base-100 p-5 lg:w-[calc(100%-16rem)] lg:gap-32"
|
||||||
>
|
>
|
||||||
<MoleculeInputModal
|
<MoleculeInputModal
|
||||||
|
@ -72,10 +83,11 @@
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { onKeyStroke, promiseTimeout } from '@vueuse/core';
|
import { onKeyStroke, promiseTimeout } from '@vueuse/core';
|
||||||
import { UnsubscribeFunc } from 'pocketbase';
|
import { RecordSubscription, UnsubscribeFunc } from 'pocketbase';
|
||||||
import { AccountsResponse, TransactionsResponse } from '../../types/pocketbase.types';
|
import { AccountsResponse, CompaniesResponse, TransactionsResponse } from '../../types/pocketbase.types';
|
||||||
import { AccountService } from '../../services/account.service';
|
import { AccountService } from '../../services/account.service';
|
||||||
import { BankService, TransactionType } from '../../services/bank.service';
|
import { AccountType, BankService, TransactionType } from '../../services/bank.service';
|
||||||
|
import { CompanyService } from '../../services/company.service';
|
||||||
import { CurrencyService } from '../../services/currency.service';
|
import { CurrencyService } from '../../services/currency.service';
|
||||||
import { ChevronDownIcon, ChevronUpIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
import { ChevronDownIcon, ChevronUpIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
||||||
import AtomHeroText from '../atoms/AtomHeroText.vue';
|
import AtomHeroText from '../atoms/AtomHeroText.vue';
|
||||||
|
@ -84,8 +96,10 @@ import MoleculeTransactionTable from '../molecules/MoleculeTransactionTable.vue'
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const subscription = ref<UnsubscribeFunc>();
|
const subscription = ref<UnsubscribeFunc>();
|
||||||
|
const companySubscription = ref<UnsubscribeFunc>();
|
||||||
|
|
||||||
const account = ref<AccountsResponse|null>();
|
const account = ref<AccountsResponse|null>();
|
||||||
|
const company = ref<CompaniesResponse|null>();
|
||||||
const transactions = ref<TransactionsResponse[]>();
|
const transactions = ref<TransactionsResponse[]>();
|
||||||
const depositModal = ref<InstanceType<typeof MoleculeInputModal>>();
|
const depositModal = ref<InstanceType<typeof MoleculeInputModal>>();
|
||||||
const withdrawModal = ref<InstanceType<typeof MoleculeInputModal>>();
|
const withdrawModal = ref<InstanceType<typeof MoleculeInputModal>>();
|
||||||
|
@ -138,14 +152,20 @@ async function getAccount() {
|
||||||
transactions.value = await BankService.getTransactions(account.value.id);
|
transactions.value = await BankService.getTransactions(account.value.id);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if(e.status == 404) {
|
try {
|
||||||
error.value = 'Der Account wurde nicht gefunden.';
|
company.value = await CompanyService.getCompanyByAccountNumber(accountId.value);
|
||||||
await promiseTimeout(4000);
|
transactions.value = await BankService.getCompanyTransactions(company.value.id);
|
||||||
error.value = '';
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} else {
|
} catch (e: any) {
|
||||||
error.value = 'Ein unerwarteter Fehler ist aufgetreten. Bitte kontaktiere einen Administrator.';
|
if(e.status == 404) {
|
||||||
// eslint-disable-next-line no-console
|
error.value = 'Der Account wurde nicht gefunden.';
|
||||||
console.error(e);
|
await promiseTimeout(4000);
|
||||||
|
error.value = '';
|
||||||
|
} else {
|
||||||
|
error.value = 'Ein unerwarteter Fehler ist aufgetreten. Bitte kontaktiere einen Administrator.';
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,50 +178,61 @@ async function handleDeposit(amount: string) {
|
||||||
const amountNumber = parseInt(amount);
|
const amountNumber = parseInt(amount);
|
||||||
if(!isNaN(amountNumber) && account.value) {
|
if(!isNaN(amountNumber) && account.value) {
|
||||||
await BankService.addTransaction(account.value.id, amountNumber, TransactionType.DEPOSIT);
|
await BankService.addTransaction(account.value.id, amountNumber, TransactionType.DEPOSIT);
|
||||||
|
} else if(!isNaN(amountNumber) && company.value) {
|
||||||
|
await BankService.addTransaction(company.value.id, amountNumber, TransactionType.DEPOSIT, AccountType.COMPANY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleWithdraw(amount: string) {
|
async function handleWithdraw(amount: string) {
|
||||||
const amountNumber = parseInt(amount);
|
const amountNumber = parseInt(amount);
|
||||||
if(!isNaN(amountNumber) && account.value) {
|
if(parseInt(amount) * 100 <= getBalance()) {
|
||||||
if(parseInt(amount) * 100 <= getBalance()) {
|
if(!isNaN(amountNumber) && account.value) {
|
||||||
// TODO: add max amount to input field and display custom error message
|
// TODO: add max amount to input field and display custom error message
|
||||||
await BankService.addTransaction(account.value.id, -amountNumber, TransactionType.WITHDRAW);
|
await BankService.addTransaction(account.value.id, -amountNumber, TransactionType.WITHDRAW);
|
||||||
} else {
|
} else if(!isNaN(amountNumber) && company.value) {
|
||||||
depositError.value = true;
|
await BankService.addTransaction(company.value.id, -amountNumber, TransactionType.WITHDRAW, AccountType.COMPANY);
|
||||||
const timeout = setTimeout(() => {
|
}
|
||||||
depositError.value = false;
|
} else {
|
||||||
clearTimeout(timeout);
|
depositError.value = true;
|
||||||
}, 5000);
|
const timeout = setTimeout(() => {
|
||||||
|
depositError.value = false;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRealtimeUpdates(transactionSubscription: RecordSubscription<TransactionsResponse>) {
|
||||||
|
if(
|
||||||
|
transactions.value &&
|
||||||
|
(transactionSubscription.record.account === account.value?.id || transactionSubscription.record.account === company.value?.id)
|
||||||
|
) {
|
||||||
|
switch (transactionSubscription.action) {
|
||||||
|
case 'create':
|
||||||
|
transactions.value.unshift(transactionSubscription.record);
|
||||||
|
break;
|
||||||
|
case 'update':
|
||||||
|
transactions.value = transactions.value.map((transaction) => {
|
||||||
|
if(transaction.id == transactionSubscription.record.id) {
|
||||||
|
transaction = transactionSubscription.record;
|
||||||
|
}
|
||||||
|
return transaction;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
transactions.value = transactions.value.filter((transaction) => transaction.id !== transactionSubscription.record.id);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
subscription.value = await BankService.subscribeToTransactionChanges((transactionSubscription) => {
|
subscription.value = await BankService.subscribeToTransactionChanges(handleRealtimeUpdates);
|
||||||
if(transactions.value && transactionSubscription.record.account === account.value?.id) {
|
companySubscription.value = await BankService.subscribeToCompanyTransactionChanges(handleRealtimeUpdates);
|
||||||
switch (transactionSubscription.action) {
|
|
||||||
case 'create':
|
|
||||||
transactions.value.unshift(transactionSubscription.record);
|
|
||||||
break;
|
|
||||||
case 'update':
|
|
||||||
transactions.value = transactions.value.map((transaction) => {
|
|
||||||
if(transaction.id == transactionSubscription.record.id) {
|
|
||||||
transaction = transactionSubscription.record;
|
|
||||||
}
|
|
||||||
return transaction;
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'delete':
|
|
||||||
transactions.value = transactions.value.filter((transaction) => transaction.id !== transactionSubscription.record.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
subscription.value?.();
|
subscription.value?.();
|
||||||
|
companySubscription.value?.();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Collections, TransactionsResponse } from '../types/pocketbase.types';
|
||||||
import { PocketbaseService } from './pocketbase.service';
|
import { PocketbaseService } from './pocketbase.service';
|
||||||
|
|
||||||
const COLLECTION = PocketbaseService.getApi().collection(Collections.Transactions);
|
const COLLECTION = PocketbaseService.getApi().collection(Collections.Transactions);
|
||||||
|
const COMPANY_COLLECTION = PocketbaseService.getApi().collection(Collections.CompanyTransactions);
|
||||||
|
|
||||||
const DEPOSIT_LABEL = 'Bargeldeinzahlung';
|
const DEPOSIT_LABEL = 'Bargeldeinzahlung';
|
||||||
const SALARY_LABEL = 'Gehalt';
|
const SALARY_LABEL = 'Gehalt';
|
||||||
|
@ -13,7 +14,14 @@ export class BankService {
|
||||||
public static async getTransactions(accountId: string): Promise<TransactionsResponse[]> {
|
public static async getTransactions(accountId: string): Promise<TransactionsResponse[]> {
|
||||||
return COLLECTION.getFullList({ filter: `account = "${accountId}"`, sort: '-created' });
|
return COLLECTION.getFullList({ filter: `account = "${accountId}"`, sort: '-created' });
|
||||||
}
|
}
|
||||||
public static async addTransaction(accountId: string, amount: number, type: TransactionType): Promise<TransactionsResponse> {
|
public static async getCompanyTransactions(companyId: string): Promise<TransactionsResponse[]> {
|
||||||
|
return COMPANY_COLLECTION.getFullList({ filter: `account = "${companyId}"`, sort: '-created' });
|
||||||
|
}
|
||||||
|
public static async addTransaction(accountId: string,
|
||||||
|
amount: number,
|
||||||
|
type: TransactionType,
|
||||||
|
accountType: AccountType = AccountType.ACCOUNT,
|
||||||
|
): Promise<TransactionsResponse> {
|
||||||
let label = '';
|
let label = '';
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TransactionType.DEPOSIT:
|
case TransactionType.DEPOSIT:
|
||||||
|
@ -29,13 +37,24 @@ export class BankService {
|
||||||
label = OPEN_ACCOUNT_LABEL;
|
label = OPEN_ACCOUNT_LABEL;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return COLLECTION.create({ account: accountId, label, amount: amount * 100 });
|
if(accountType === AccountType.COMPANY) {
|
||||||
|
return COMPANY_COLLECTION.create({ account: accountId, label, amount: amount * 100 });
|
||||||
|
} else {
|
||||||
|
return COLLECTION.create({ account: accountId, label, amount: amount * 100 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public static async subscribeToTransactionChanges(callback: (data: RecordSubscription<TransactionsResponse>)=> void): Promise<UnsubscribeFunc> {
|
public static async subscribeToTransactionChanges(callback: (data: RecordSubscription<TransactionsResponse>)=> void): Promise<UnsubscribeFunc> {
|
||||||
return await COLLECTION.subscribe<TransactionsResponse>('*', (data) => {
|
return await COLLECTION.subscribe<TransactionsResponse>('*', (data) => {
|
||||||
callback(data);
|
callback(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
public static async subscribeToCompanyTransactionChanges(
|
||||||
|
callback: (data: RecordSubscription<TransactionsResponse>)=> void,
|
||||||
|
): Promise<UnsubscribeFunc> {
|
||||||
|
return await COMPANY_COLLECTION.subscribe<TransactionsResponse>('*', (data) => {
|
||||||
|
callback(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TransactionType {
|
export enum TransactionType {
|
||||||
|
@ -43,4 +62,9 @@ export enum TransactionType {
|
||||||
SALARY,
|
SALARY,
|
||||||
WITHDRAW,
|
WITHDRAW,
|
||||||
OPEN_ACCOUNT,
|
OPEN_ACCOUNT,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AccountType {
|
||||||
|
ACCOUNT,
|
||||||
|
COMPANY,
|
||||||
}
|
}
|
|
@ -40,4 +40,7 @@ export class CompanyService {
|
||||||
}
|
}
|
||||||
throw new Error('Not authenticated');
|
throw new Error('Not authenticated');
|
||||||
}
|
}
|
||||||
|
public static async getCompanyByAccountNumber(accountNumber: string): Promise<CompaniesResponse> {
|
||||||
|
return COLLECTION.getFirstListItem(`accountNumber="${accountNumber}"`);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -6,6 +6,7 @@ export enum Collections {
|
||||||
Accounts = "accounts",
|
Accounts = "accounts",
|
||||||
AccountsList = "accountsList",
|
AccountsList = "accountsList",
|
||||||
Companies = "companies",
|
Companies = "companies",
|
||||||
|
CompanyTransactions = "companyTransactions",
|
||||||
Settings = "settings",
|
Settings = "settings",
|
||||||
Transactions = "transactions",
|
Transactions = "transactions",
|
||||||
}
|
}
|
||||||
|
@ -54,9 +55,16 @@ export type AccountsListRecord<Tbalance = unknown, Tname = unknown> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompaniesRecord = {
|
export type CompaniesRecord = {
|
||||||
|
accountNumber?: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CompanyTransactionsRecord = {
|
||||||
|
account: RecordIdString
|
||||||
|
label: string
|
||||||
|
amount?: number
|
||||||
|
}
|
||||||
|
|
||||||
export type SettingsRecord = {
|
export type SettingsRecord = {
|
||||||
minWage: number
|
minWage: number
|
||||||
incomeTax: number
|
incomeTax: number
|
||||||
|
@ -74,6 +82,7 @@ export type TransactionsRecord = {
|
||||||
export type AccountsResponse<Texpand = unknown> = Required<AccountsRecord> & BaseSystemFields<Texpand>
|
export type AccountsResponse<Texpand = unknown> = Required<AccountsRecord> & BaseSystemFields<Texpand>
|
||||||
export type AccountsListResponse<Tbalance = unknown, Tname = unknown, Texpand = unknown> = Required<AccountsListRecord<Tbalance, Tname>> & BaseSystemFields<Texpand>
|
export type AccountsListResponse<Tbalance = unknown, Tname = unknown, Texpand = unknown> = Required<AccountsListRecord<Tbalance, Tname>> & BaseSystemFields<Texpand>
|
||||||
export type CompaniesResponse<Texpand = unknown> = Required<CompaniesRecord> & AuthSystemFields<Texpand>
|
export type CompaniesResponse<Texpand = unknown> = Required<CompaniesRecord> & AuthSystemFields<Texpand>
|
||||||
|
export type CompanyTransactionsResponse<Texpand = unknown> = Required<CompanyTransactionsRecord> & BaseSystemFields<Texpand>
|
||||||
export type SettingsResponse<Texpand = unknown> = Required<SettingsRecord> & BaseSystemFields<Texpand>
|
export type SettingsResponse<Texpand = unknown> = Required<SettingsRecord> & BaseSystemFields<Texpand>
|
||||||
export type TransactionsResponse<Texpand = unknown> = Required<TransactionsRecord> & BaseSystemFields<Texpand>
|
export type TransactionsResponse<Texpand = unknown> = Required<TransactionsRecord> & BaseSystemFields<Texpand>
|
||||||
|
|
||||||
|
@ -83,6 +92,7 @@ export type CollectionRecords = {
|
||||||
accounts: AccountsRecord
|
accounts: AccountsRecord
|
||||||
accountsList: AccountsListRecord
|
accountsList: AccountsListRecord
|
||||||
companies: CompaniesRecord
|
companies: CompaniesRecord
|
||||||
|
companyTransactions: CompanyTransactionsRecord
|
||||||
settings: SettingsRecord
|
settings: SettingsRecord
|
||||||
transactions: TransactionsRecord
|
transactions: TransactionsRecord
|
||||||
}
|
}
|
||||||
|
@ -91,6 +101,7 @@ export type CollectionResponses = {
|
||||||
accounts: AccountsResponse
|
accounts: AccountsResponse
|
||||||
accountsList: AccountsListResponse
|
accountsList: AccountsListResponse
|
||||||
companies: CompaniesResponse
|
companies: CompaniesResponse
|
||||||
|
companyTransactions: CompanyTransactionsResponse
|
||||||
settings: SettingsResponse
|
settings: SettingsResponse
|
||||||
transactions: TransactionsResponse
|
transactions: TransactionsResponse
|
||||||
}
|
}
|
Loading…
Reference in a new issue