1
0
Fork 0
forked from Kispi/Core

feat(webapp): add account modal functionality

This commit is contained in:
Simon Giesel 2022-07-28 16:46:07 +02:00
parent 6425af1ffb
commit dda5a1ac7d
12 changed files with 276 additions and 25 deletions

View file

@ -6,15 +6,26 @@
class="modal-toggle" class="modal-toggle"
@change="$emit('open', {id, open: ($event.target as HTMLInputElement).checked})" @change="$emit('open', {id, open: ($event.target as HTMLInputElement).checked})"
/> />
<div class="modal"> <label
<div class="modal-box"> :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> <h3 class="font-bold text-lg">{{ title }}</h3>
<p class="py-4"><slot /></p> <p class="py-4"><slot /></p>
<div class="modal-action"> <div class="modal-action">
<slot name="action" /> <slot name="action" />
</div> </div>
</div> </label>
</div> </label>
</Teleport> </Teleport>
</template> </template>
@ -28,6 +39,14 @@ defineProps({
type: String, type: String,
required: true, required: true,
}, },
darker: {
type: Boolean,
default: false,
},
closeButton: {
type: Boolean,
default: false,
},
}); });
defineEmits(['open']); defineEmits(['open']);

View file

@ -1,5 +1,8 @@
<template> <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 <option
disabled disabled
selected selected
@ -22,7 +25,13 @@ defineProps({
type: Array as PropType<string[]>, type: Array as PropType<string[]>,
required: true, required: true,
}, },
modelValue: {
type: String,
default: '',
},
}); });
defineEmits(['update:modelValue']);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -10,52 +10,73 @@
v-model="firstName" v-model="firstName"
placeholder="Vorname" placeholder="Vorname"
/> />
<AtomInput placeholder="Nachname" /> <AtomInput
v-model="lastName"
placeholder="Nachname"
/>
</div> </div>
<AtomInput <AtomInput
v-model="accountNumber"
placeholder="Kontonummer" placeholder="Kontonummer"
type="number" type="number"
/> />
<AtomInput <AtomInput
v-model="birthday"
placeholder="Geburtsdatum" placeholder="Geburtsdatum"
type="number"
/> />
<div class="flex gap-4"> <div class="flex gap-4">
<AtomInput placeholder="Straße" />
<AtomInput <AtomInput
v-model="street"
placeholder="Straße"
/>
<AtomInput
v-model="houseNumber"
placeholder="Hnr." placeholder="Hnr."
class="w-3/12" class="w-3/12"
/> />
</div> </div>
<div class="flex gap-4"> <div class="flex gap-4">
<AtomInput <AtomInput
v-model="zipCode"
placeholder="Postleitzahl" placeholder="Postleitzahl"
class="w-6/12" class="w-6/12"
/> />
<AtomInput placeholder="Stadt" /> <AtomInput
v-model="city"
placeholder="Stadt"
/>
</div> </div>
<h4 class="text-md">Kontakt</h4> <h4 class="text-md">Kontakt</h4>
<div class="flex gap-4"> <div
v-for="index in contactCount"
class="flex gap-4"
>
<AtomSelect <AtomSelect
v-model="contactLabel[index - 1]"
placeholder="Label" placeholder="Label"
:options="['Festnetz', 'Mobil', 'Arbeit']" :options="['Festnetz', 'Mobil', 'Arbeit']"
class="w-4/12" class="w-4/12"
@change="handleContactChange(index)"
/>
<AtomInput
v-model="contactPhone[index - 1]"
:disabled="contactLabel.length < index"
placeholder="Telefonnummer"
/> />
<AtomInput placeholder="Telefonnummer" />
</div> </div>
</div> </div>
<template #action> <template #action>
<label <label
ref="abortButton" ref="abortButton"
class="btn gap-2"
:for="id" :for="id"
class="btn gap-2"
> >
<XCircleIcon class="w-6 h-6" /> <XCircleIcon class="w-6 h-6" />
Abbrechen Abbrechen
</label> </label>
<button <button
class="btn gap-2" class="btn gap-2"
@click="handleSubmit()" @click="handleSubmit"
> >
<CheckCircleIcon class="w-6 h-6" /> <CheckCircleIcon class="w-6 h-6" />
Speichern Speichern
@ -66,12 +87,24 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { DataService } from '../services/data.service';
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/outline'; import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/outline';
import AtomInput from '../atoms/AtomInput.vue'; import AtomInput from '../atoms/AtomInput.vue';
import AtomModal from '../atoms/AtomModal.vue'; import AtomModal from '../atoms/AtomModal.vue';
import AtomSelect from '../atoms/AtomSelect.vue'; import AtomSelect from '../atoms/AtomSelect.vue';
const abortButton = ref();
const firstName = 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({ defineProps({
id: { id: {
@ -80,10 +113,53 @@ defineProps({
}, },
}); });
function handleSubmit() { async function handleSubmit() {
console.log('TODO: Submit'); try {
console.log(firstName.value); 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

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

View file

@ -24,12 +24,14 @@
<span v-else-if="header.type === TableHeaderType.CURRENCY"> <span v-else-if="header.type === TableHeaderType.CURRENCY">
{{ CurrencyService.toString(entry[header.key]) }} {{ CurrencyService.toString(entry[header.key]) }}
</span> </span>
<button <label
v-if="header.type === TableHeaderType.BUTTON_CONTACT" v-if="header.type === TableHeaderType.BUTTON_CONTACT"
:for="contactModalId"
class="btn btn-sm btn-ghost p-1" 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" /> <DocumentTextIcon class="h-6 w-6" />
</button> </label>
<button <button
v-if="header.type === TableHeaderType.BUTTON_ACCOUNT" v-if="header.type === TableHeaderType.BUTTON_ACCOUNT"
class="btn btn-sm btn-ghost p-1" class="btn btn-sm btn-ghost p-1"
@ -60,7 +62,13 @@ defineProps({
type: Array as PropType<any[]>, type: Array as PropType<any[]>,
required: true, required: true,
}, },
contactModalId: {
type: String,
required: true,
},
}); });
defineEmits(['open-contact-modal']);
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -39,4 +39,8 @@ export const AccountService = {
} while(accountDocuments.total > offset); } while(accountDocuments.total > offset);
return accounts; return accounts;
}, },
async addAccount(account: IAccount): Promise<IAccount> {
const accountDocument = await database.createDocument<IAccount & Models.Document>(ACCOUNTS_COLLECTION_ID, 'unique()', account);
return accountDocument;
},
}; };

View file

@ -9,6 +9,7 @@ const TRANSACTIONS_COLLECTION_ID = '62dfd81b7671727b27cd';
const DEPOSIT_LABEL = 'Bargeldeinzahlung'; const DEPOSIT_LABEL = 'Bargeldeinzahlung';
const SALARY_LABEL = 'Gehalt'; const SALARY_LABEL = 'Gehalt';
const WITHDRAW_LABEL = 'Bargeldauszahlung'; const WITHDRAW_LABEL = 'Bargeldauszahlung';
const OPEN_ACCOUNT_LABEL = 'Kontoeröffnung';
const sdk = AppwriteService.getSDK(); const sdk = AppwriteService.getSDK();
export const BankService = { export const BankService = {
@ -32,9 +33,24 @@ export const BankService = {
}, },
async addTransaction(accountNumber: string, amount: number, type: TransactionType): Promise<ITransaction> { async addTransaction(accountNumber: string, amount: number, type: TransactionType): Promise<ITransaction> {
const database = new Databases(sdk, DATABASE_ID); 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()', { const transactionDocument = await database.createDocument<ITransaction & Models.Document>(TRANSACTIONS_COLLECTION_ID, 'unique()', {
accountNumber: accountNumber, accountNumber: accountNumber,
label: type === TransactionType.DEPOSIT ? DEPOSIT_LABEL : (type === TransactionType.SALARY ? SALARY_LABEL : WITHDRAW_LABEL), label,
amount: (type === TransactionType.WITHDRAW ? -amount : amount), amount: (type === TransactionType.WITHDRAW ? -amount : amount),
}); });
await AccountService.updateBalance(accountNumber, type === TransactionType.WITHDRAW ? -amount : amount); await AccountService.updateBalance(accountNumber, type === TransactionType.WITHDRAW ? -amount : amount);
@ -46,4 +62,5 @@ export enum TransactionType {
DEPOSIT, DEPOSIT,
SALARY, SALARY,
WITHDRAW, WITHDRAW,
OPEN_ACCOUNT,
} }

View file

@ -3,7 +3,11 @@ import { AccountService } from './account.service';
import { AppwriteService } from './appwrite.service'; import { AppwriteService } from './appwrite.service';
import { IAccountData } from '../../interfaces/account-data.interface'; import { IAccountData } from '../../interfaces/account-data.interface';
import { IPersonalInformation } from '../../interfaces/personal-information.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 DATABASE_ID = '62dfd5787755e7ed9fcd';
const PERSONAL_INFORMATION_COLLECTION_ID = '62e004abb0cdae53b483'; const PERSONAL_INFORMATION_COLLECTION_ID = '62e004abb0cdae53b483';
const sdk = AppwriteService.getSDK(); const sdk = AppwriteService.getSDK();
@ -23,6 +27,7 @@ export const DataService = {
account.name = `${account.firstName} ${account.lastName}`; account.name = `${account.firstName} ${account.lastName}`;
account.birthday = personalInformation.birthday; account.birthday = personalInformation.birthday;
account.address = personalInformation.address; account.address = personalInformation.address;
account.contact = personalInformation.contact;
} }
}, },
); );
@ -30,4 +35,31 @@ export const DataService = {
} while(personalInformationDocuments.total > offset); } while(personalInformationDocuments.total > offset);
return accounts; 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;
},
}; };

View file

@ -4,8 +4,18 @@
v-if="data" v-if="data"
:table-headers="tableHeaders" :table-headers="tableHeaders"
:data="data" :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 <label
class="btn btn-primary btn-lg btn-circle shadow-2xl fixed bottom-8 right-8" class="btn btn-primary btn-lg btn-circle shadow-2xl fixed bottom-8 right-8"
:for="addAccountModalId" :for="addAccountModalId"
@ -18,11 +28,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { DataService } from '../services/data.service'; import { DataService } from '../services/data.service';
import { IAccountData } from '../../interfaces/account-data.interface';
import { PlusIcon } from '@heroicons/vue/outline'; import { PlusIcon } from '@heroicons/vue/outline';
import MoleculeAddAccountModal from '../molecules/MoleculeAddAccountModal.vue'; import MoleculeAddAccountModal from '../molecules/MoleculeAddAccountModal.vue';
import MoleculeContactModal from '../molecules/MoleculeContactModal.vue';
import MoleculeDataTable, { TableHeaderType } from '../molecules/MoleculeDataTable.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 addAccountModalId = 'add-account-modal';
const tableHeaders = [ const tableHeaders = [
{ {
@ -38,7 +53,7 @@ const tableHeaders = [
{ {
title: 'Geburtsdatum', title: 'Geburtsdatum',
key: 'birthday', key: 'birthday',
type: TableHeaderType.DATE, type: TableHeaderType.STRING,
}, },
{ {
title: 'Kontostand', title: 'Kontostand',
@ -70,8 +85,16 @@ const tableHeaders = [
onMounted(async () => { onMounted(async () => {
data.value = await DataService.getAllData(); 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
</style> </style>

View file

@ -2,6 +2,7 @@ import { IAccount } from './account.interface';
export interface IAccountData extends IAccount { export interface IAccountData extends IAccount {
name: string; name: string;
birthday: number; birthday: string;
address: string; address: string;
contact: string[];
} }

View file

@ -0,0 +1,8 @@
export interface ICreateAccount {
accountNumber: string;
firstName: string;
lastName: string;
birthday: string;
address: string;
contact: string[];
}

View file

@ -1,5 +1,6 @@
export interface IPersonalInformation { export interface IPersonalInformation {
accountNumber: string; accountNumber: string;
birthday: number; birthday: string;
address: string; address: string;
contact: string[];
} }