forked from Kispi/Core
feat(webapp): add account modal functionality
This commit is contained in:
parent
6425af1ffb
commit
dda5a1ac7d
12 changed files with 276 additions and 25 deletions
|
@ -6,15 +6,26 @@
|
|||
class="modal-toggle"
|
||||
@change="$emit('open', {id, open: ($event.target as HTMLInputElement).checked})"
|
||||
/>
|
||||
<div class="modal">
|
||||
<div class="modal-box">
|
||||
<label
|
||||
:for="id"
|
||||
class="modal cursor-pointer"
|
||||
>
|
||||
<label
|
||||
:class="`modal-box${darker ? ' bg-base-300' : ''}`"
|
||||
for=""
|
||||
>
|
||||
<label
|
||||
v-if="closeButton"
|
||||
:for="id"
|
||||
class="btn btn-sm btn-circle absolute right-2 top-2"
|
||||
>✕</label>
|
||||
<h3 class="font-bold text-lg">{{ title }}</h3>
|
||||
<p class="py-4"><slot /></p>
|
||||
<div class="modal-action">
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
|
@ -28,6 +39,14 @@ defineProps({
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
darker: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
closeButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['open']);
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<select class="select select-bordered w-full">
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
>
|
||||
<option
|
||||
disabled
|
||||
selected
|
||||
|
@ -22,7 +25,13 @@ defineProps({
|
|||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['update:modelValue']);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -10,52 +10,73 @@
|
|||
v-model="firstName"
|
||||
placeholder="Vorname"
|
||||
/>
|
||||
<AtomInput placeholder="Nachname" />
|
||||
<AtomInput
|
||||
v-model="lastName"
|
||||
placeholder="Nachname"
|
||||
/>
|
||||
</div>
|
||||
<AtomInput
|
||||
v-model="accountNumber"
|
||||
placeholder="Kontonummer"
|
||||
type="number"
|
||||
/>
|
||||
<AtomInput
|
||||
v-model="birthday"
|
||||
placeholder="Geburtsdatum"
|
||||
type="number"
|
||||
/>
|
||||
<div class="flex gap-4">
|
||||
<AtomInput placeholder="Straße" />
|
||||
<AtomInput
|
||||
v-model="street"
|
||||
placeholder="Straße"
|
||||
/>
|
||||
<AtomInput
|
||||
v-model="houseNumber"
|
||||
placeholder="Hnr."
|
||||
class="w-3/12"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<AtomInput
|
||||
v-model="zipCode"
|
||||
placeholder="Postleitzahl"
|
||||
class="w-6/12"
|
||||
/>
|
||||
<AtomInput placeholder="Stadt" />
|
||||
<AtomInput
|
||||
v-model="city"
|
||||
placeholder="Stadt"
|
||||
/>
|
||||
</div>
|
||||
<h4 class="text-md">Kontakt</h4>
|
||||
<div class="flex gap-4">
|
||||
<div
|
||||
v-for="index in contactCount"
|
||||
class="flex gap-4"
|
||||
>
|
||||
<AtomSelect
|
||||
v-model="contactLabel[index - 1]"
|
||||
placeholder="Label"
|
||||
:options="['Festnetz', 'Mobil', 'Arbeit']"
|
||||
class="w-4/12"
|
||||
@change="handleContactChange(index)"
|
||||
/>
|
||||
<AtomInput
|
||||
v-model="contactPhone[index - 1]"
|
||||
:disabled="contactLabel.length < index"
|
||||
placeholder="Telefonnummer"
|
||||
/>
|
||||
<AtomInput placeholder="Telefonnummer" />
|
||||
</div>
|
||||
</div>
|
||||
<template #action>
|
||||
<label
|
||||
ref="abortButton"
|
||||
class="btn gap-2"
|
||||
:for="id"
|
||||
class="btn gap-2"
|
||||
>
|
||||
<XCircleIcon class="w-6 h-6" />
|
||||
Abbrechen
|
||||
</label>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
@click="handleSubmit()"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<CheckCircleIcon class="w-6 h-6" />
|
||||
Speichern
|
||||
|
@ -66,12 +87,24 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/outline';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
import AtomSelect from '../atoms/AtomSelect.vue';
|
||||
|
||||
const abortButton = ref();
|
||||
const firstName = ref('');
|
||||
const lastName = ref('');
|
||||
const accountNumber = ref('');
|
||||
const birthday = ref('');
|
||||
const street = ref('');
|
||||
const houseNumber = ref('');
|
||||
const zipCode = ref('');
|
||||
const city = ref('');
|
||||
const contactLabel = ref([]);
|
||||
const contactPhone = ref([]);
|
||||
const contactCount = ref(1);
|
||||
|
||||
defineProps({
|
||||
id: {
|
||||
|
@ -80,10 +113,53 @@ defineProps({
|
|||
},
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
console.log('TODO: Submit');
|
||||
console.log(firstName.value);
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const account = await DataService.addAccount(
|
||||
{
|
||||
accountNumber: accountNumber.value,
|
||||
firstName: firstName.value,
|
||||
lastName: lastName.value,
|
||||
birthday: birthday.value,
|
||||
address: `${street.value} ${houseNumber.value}, ${zipCode.value} ${city.value}`,
|
||||
contact: contactLabel.value.map((label, index) => JSON.stringify({
|
||||
label,
|
||||
phone: contactPhone.value[index],
|
||||
})),
|
||||
},
|
||||
);
|
||||
emit('submit', account);
|
||||
firstName.value = '';
|
||||
lastName.value = '';
|
||||
accountNumber.value = '';
|
||||
birthday.value = '';
|
||||
street.value = '';
|
||||
houseNumber.value = '';
|
||||
zipCode.value = '';
|
||||
city.value = '';
|
||||
contactLabel.value = [];
|
||||
contactPhone.value = [];
|
||||
contactCount.value = 1;
|
||||
abortButton.value.click();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
if(error.message == 'Account already exists') {
|
||||
alert('Konto existiert bereits');
|
||||
} else {
|
||||
alert('Fehler beim Speichern');
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleContactChange(index: number) {
|
||||
if(index === contactCount.value) {
|
||||
contactCount.value++;
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(['submit']);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
53
src/components/molecules/MoleculeContactModal.vue
Normal file
53
src/components/molecules/MoleculeContactModal.vue
Normal file
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<AtomModal
|
||||
:id="id"
|
||||
:title="`Kontaktdaten von ${name}`"
|
||||
darker
|
||||
close-button
|
||||
>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<tbody>
|
||||
<tr v-for="entry of contact">
|
||||
<td>{{ JSON.parse(entry).label }}</td>
|
||||
<td>{{ JSON.parse(entry).phone }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
v-if="contact.length == 0"
|
||||
class="alert alert-info shadow-lg"
|
||||
>
|
||||
<div>
|
||||
<InformationCircleIcon class="h-6 w-6" />
|
||||
<span>Keine Kontaktdaten vorhanden.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AtomModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
import { InformationCircleIcon } from '@heroicons/vue/outline';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
|
||||
defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
contact: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -24,12 +24,14 @@
|
|||
<span v-else-if="header.type === TableHeaderType.CURRENCY">
|
||||
{{ CurrencyService.toString(entry[header.key]) }}
|
||||
</span>
|
||||
<button
|
||||
<label
|
||||
v-if="header.type === TableHeaderType.BUTTON_CONTACT"
|
||||
:for="contactModalId"
|
||||
class="btn btn-sm btn-ghost p-1"
|
||||
@click="$emit('open-contact-modal', { name: entry.name, contact: entry.contact })"
|
||||
>
|
||||
<DocumentTextIcon class="h-6 w-6" />
|
||||
</button>
|
||||
</label>
|
||||
<button
|
||||
v-if="header.type === TableHeaderType.BUTTON_ACCOUNT"
|
||||
class="btn btn-sm btn-ghost p-1"
|
||||
|
@ -60,7 +62,13 @@ defineProps({
|
|||
type: Array as PropType<any[]>,
|
||||
required: true,
|
||||
},
|
||||
contactModalId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['open-contact-modal']);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -39,4 +39,8 @@ export const AccountService = {
|
|||
} while(accountDocuments.total > offset);
|
||||
return accounts;
|
||||
},
|
||||
async addAccount(account: IAccount): Promise<IAccount> {
|
||||
const accountDocument = await database.createDocument<IAccount & Models.Document>(ACCOUNTS_COLLECTION_ID, 'unique()', account);
|
||||
return accountDocument;
|
||||
},
|
||||
};
|
|
@ -9,6 +9,7 @@ const TRANSACTIONS_COLLECTION_ID = '62dfd81b7671727b27cd';
|
|||
const DEPOSIT_LABEL = 'Bargeldeinzahlung';
|
||||
const SALARY_LABEL = 'Gehalt';
|
||||
const WITHDRAW_LABEL = 'Bargeldauszahlung';
|
||||
const OPEN_ACCOUNT_LABEL = 'Kontoeröffnung';
|
||||
const sdk = AppwriteService.getSDK();
|
||||
|
||||
export const BankService = {
|
||||
|
@ -32,9 +33,24 @@ export const BankService = {
|
|||
},
|
||||
async addTransaction(accountNumber: string, amount: number, type: TransactionType): Promise<ITransaction> {
|
||||
const database = new Databases(sdk, DATABASE_ID);
|
||||
let label = '';
|
||||
switch (type) {
|
||||
case TransactionType.DEPOSIT:
|
||||
label = DEPOSIT_LABEL;
|
||||
break;
|
||||
case TransactionType.SALARY:
|
||||
label = SALARY_LABEL;
|
||||
break;
|
||||
case TransactionType.WITHDRAW:
|
||||
label = WITHDRAW_LABEL;
|
||||
break;
|
||||
case TransactionType.OPEN_ACCOUNT:
|
||||
label = OPEN_ACCOUNT_LABEL;
|
||||
break;
|
||||
}
|
||||
const transactionDocument = await database.createDocument<ITransaction & Models.Document>(TRANSACTIONS_COLLECTION_ID, 'unique()', {
|
||||
accountNumber: accountNumber,
|
||||
label: type === TransactionType.DEPOSIT ? DEPOSIT_LABEL : (type === TransactionType.SALARY ? SALARY_LABEL : WITHDRAW_LABEL),
|
||||
label,
|
||||
amount: (type === TransactionType.WITHDRAW ? -amount : amount),
|
||||
});
|
||||
await AccountService.updateBalance(accountNumber, type === TransactionType.WITHDRAW ? -amount : amount);
|
||||
|
@ -46,4 +62,5 @@ export enum TransactionType {
|
|||
DEPOSIT,
|
||||
SALARY,
|
||||
WITHDRAW,
|
||||
OPEN_ACCOUNT,
|
||||
}
|
|
@ -3,7 +3,11 @@ import { AccountService } from './account.service';
|
|||
import { AppwriteService } from './appwrite.service';
|
||||
import { IAccountData } from '../../interfaces/account-data.interface';
|
||||
import { IPersonalInformation } from '../../interfaces/personal-information.interface';
|
||||
import { ICreateAccount } from '../../interfaces/create-account.interface';
|
||||
import { IAccount } from '../../interfaces/account.interface';
|
||||
import { BankService, TransactionType } from './bank.service';
|
||||
|
||||
const OPEN_ACCOUNT_BALANCE = 5;
|
||||
const DATABASE_ID = '62dfd5787755e7ed9fcd';
|
||||
const PERSONAL_INFORMATION_COLLECTION_ID = '62e004abb0cdae53b483';
|
||||
const sdk = AppwriteService.getSDK();
|
||||
|
@ -23,6 +27,7 @@ export const DataService = {
|
|||
account.name = `${account.firstName} ${account.lastName}`;
|
||||
account.birthday = personalInformation.birthday;
|
||||
account.address = personalInformation.address;
|
||||
account.contact = personalInformation.contact;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -30,4 +35,31 @@ export const DataService = {
|
|||
} while(personalInformationDocuments.total > offset);
|
||||
return accounts;
|
||||
},
|
||||
async addAccount(account: ICreateAccount): Promise<IAccountData> {
|
||||
if(await AccountService.getAccount(account.accountNumber)) {
|
||||
throw new Error('Account already exists');
|
||||
}
|
||||
const accountDocument = await AccountService.addAccount({
|
||||
accountNumber: account.accountNumber,
|
||||
firstName: account.firstName,
|
||||
lastName: account.lastName,
|
||||
balance: 0,
|
||||
} as IAccount) as IAccountData;
|
||||
await BankService.addTransaction(account.accountNumber, OPEN_ACCOUNT_BALANCE, TransactionType.OPEN_ACCOUNT);
|
||||
const personalInformation = await database.createDocument<IAccountData & Models.Document>(
|
||||
PERSONAL_INFORMATION_COLLECTION_ID, 'unique()', {
|
||||
accountNumber: account.accountNumber,
|
||||
birthday: account.birthday,
|
||||
address: account.address,
|
||||
contact: account.contact,
|
||||
} as IPersonalInformation);
|
||||
if(accountDocument) {
|
||||
accountDocument.name = `${accountDocument.firstName} ${accountDocument.lastName}`;
|
||||
accountDocument.birthday = personalInformation.birthday;
|
||||
accountDocument.address = personalInformation.address;
|
||||
}
|
||||
// manually edit balance as the TransactionService updates the value after a transaction was created
|
||||
accountDocument.balance = OPEN_ACCOUNT_BALANCE;
|
||||
return accountDocument;
|
||||
},
|
||||
};
|
|
@ -4,8 +4,18 @@
|
|||
v-if="data"
|
||||
:table-headers="tableHeaders"
|
||||
:data="data"
|
||||
:contact-modal-id="contactModalId"
|
||||
@open-contact-modal="openContactModal"
|
||||
/>
|
||||
<MoleculeContactModal
|
||||
:id="contactModalId"
|
||||
:name="contactName"
|
||||
:contact="contact"
|
||||
/>
|
||||
<MoleculeAddAccountModal
|
||||
:id="addAccountModalId"
|
||||
@submit="addAccount"
|
||||
/>
|
||||
<MoleculeAddAccountModal :id="addAccountModalId" />
|
||||
<label
|
||||
class="btn btn-primary btn-lg btn-circle shadow-2xl fixed bottom-8 right-8"
|
||||
:for="addAccountModalId"
|
||||
|
@ -18,11 +28,16 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { IAccountData } from '../../interfaces/account-data.interface';
|
||||
import { PlusIcon } from '@heroicons/vue/outline';
|
||||
import MoleculeAddAccountModal from '../molecules/MoleculeAddAccountModal.vue';
|
||||
import MoleculeContactModal from '../molecules/MoleculeContactModal.vue';
|
||||
import MoleculeDataTable, { TableHeaderType } from '../molecules/MoleculeDataTable.vue';
|
||||
|
||||
const data = ref();
|
||||
const data = ref<IAccountData[]>([]);
|
||||
const contactName = ref('');
|
||||
const contact = ref<string[]>([]);
|
||||
const contactModalId = 'contact-modal';
|
||||
const addAccountModalId = 'add-account-modal';
|
||||
const tableHeaders = [
|
||||
{
|
||||
|
@ -38,7 +53,7 @@ const tableHeaders = [
|
|||
{
|
||||
title: 'Geburtsdatum',
|
||||
key: 'birthday',
|
||||
type: TableHeaderType.DATE,
|
||||
type: TableHeaderType.STRING,
|
||||
},
|
||||
{
|
||||
title: 'Kontostand',
|
||||
|
@ -70,8 +85,16 @@ const tableHeaders = [
|
|||
onMounted(async () => {
|
||||
data.value = await DataService.getAllData();
|
||||
});
|
||||
|
||||
function openContactModal(data: { name: string, contact: string[] }) {
|
||||
contactName.value = data.name;
|
||||
contact.value = data.contact;
|
||||
}
|
||||
|
||||
function addAccount(account: IAccountData) {
|
||||
data.value.push(account);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -2,6 +2,7 @@ import { IAccount } from './account.interface';
|
|||
|
||||
export interface IAccountData extends IAccount {
|
||||
name: string;
|
||||
birthday: number;
|
||||
birthday: string;
|
||||
address: string;
|
||||
contact: string[];
|
||||
}
|
8
src/interfaces/create-account.interface.ts
Normal file
8
src/interfaces/create-account.interface.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export interface ICreateAccount {
|
||||
accountNumber: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
birthday: string;
|
||||
address: string;
|
||||
contact: string[];
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
export interface IPersonalInformation {
|
||||
accountNumber: string;
|
||||
birthday: number;
|
||||
birthday: string;
|
||||
address: string;
|
||||
contact: string[];
|
||||
}
|
Loading…
Reference in a new issue