1
0
Fork 0
forked from Kispi/Core

feat(core): add company bank accounts
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Simon Giesel 2023-07-11 13:38:58 +02:00
parent 5aea42d33a
commit a2d4d89776
5 changed files with 178 additions and 45 deletions

View file

@ -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": {}
} }
] ]

View file

@ -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>

View file

@ -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,
} }

View file

@ -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}"`);
}
} }

View file

@ -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
} }