Added standard form components, account creation and editing.
This commit is contained in:
parent
ae94dcebe6
commit
ace83d49ba
|
@ -11,6 +11,7 @@ import handy_http_handlers.path_handler;
|
|||
import profile.service;
|
||||
import account.model;
|
||||
import util.money;
|
||||
import account.data;
|
||||
|
||||
/// The data the API provides for an Account entity.
|
||||
struct AccountResponse {
|
||||
|
@ -79,6 +80,25 @@ void handleCreateAccount(ref ServerHttpRequest request, ref ServerHttpResponse r
|
|||
writeJsonBody(response, AccountResponse.of(account));
|
||||
}
|
||||
|
||||
void handleUpdateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request);
|
||||
auto ds = getProfileDataSource(request);
|
||||
AccountRepository repo = ds.getAccountRepository();
|
||||
auto account = repo.findById(accountId).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
|
||||
Account updated = repo.update(accountId, Account(
|
||||
account.id,
|
||||
account.createdAt,
|
||||
account.archived,
|
||||
AccountType.fromId(payload.type),
|
||||
payload.numberSuffix,
|
||||
payload.name,
|
||||
Currency.ofCode(payload.currency),
|
||||
payload.description
|
||||
));
|
||||
writeJsonBody(response, AccountResponse.of(updated));
|
||||
}
|
||||
|
||||
void handleDeleteAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
auto ds = getProfileDataSource(request);
|
||||
|
|
|
@ -70,7 +70,8 @@ SQL",
|
|||
newData.numberSuffix,
|
||||
newData.name,
|
||||
newData.currency.code,
|
||||
newData.description
|
||||
newData.description,
|
||||
id
|
||||
);
|
||||
return this.findById(id).orElseThrow("Account doesn't exist");
|
||||
});
|
||||
|
|
|
@ -49,6 +49,7 @@ HttpRequestHandler mapApiHandlers() {
|
|||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/accounts", &handleGetAccounts);
|
||||
a.map(HttpMethod.POST, PROFILE_PATH ~ "/accounts", &handleCreateAccount);
|
||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/accounts/:accountId:ulong", &handleGetAccount);
|
||||
a.map(HttpMethod.PUT, PROFILE_PATH ~ "/accounts/:accountId:ulong", &handleUpdateAccount);
|
||||
a.map(HttpMethod.DELETE, PROFILE_PATH ~ "/accounts/:accountId:ulong", &handleDeleteAccount);
|
||||
|
||||
import transaction.api;
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import ModalWrapper from './components/ModalWrapper.vue';
|
||||
|
||||
import GlobalAlertModal from './components/GlobalAlertModal.vue';
|
||||
</script>
|
||||
<template>
|
||||
<RouterView></RouterView>
|
||||
|
||||
<!-- Global alert modal used by util/alert.ts -->
|
||||
<ModalWrapper id="global-alert-modal">
|
||||
<p id="global-alert-modal-text">This is an alert!</p>
|
||||
</ModalWrapper>
|
||||
<GlobalAlertModal id="global-alert-modal" />
|
||||
</template>
|
||||
<style scoped></style>
|
||||
|
|
|
@ -1,6 +1,43 @@
|
|||
import { ApiClient } from './base'
|
||||
import type { Profile } from './profile'
|
||||
|
||||
export interface AccountType {
|
||||
id: string
|
||||
name: string
|
||||
debitsPositive: boolean
|
||||
}
|
||||
|
||||
export abstract class AccountTypes {
|
||||
public static readonly CHECKING: AccountType = {
|
||||
id: 'CHECKING',
|
||||
name: 'Checking',
|
||||
debitsPositive: true,
|
||||
}
|
||||
public static readonly SAVINGS: AccountType = {
|
||||
id: 'SAVINGS',
|
||||
name: 'Savings',
|
||||
debitsPositive: true,
|
||||
}
|
||||
public static readonly CREDIT_CARD: AccountType = {
|
||||
id: 'CREDIT_CARD',
|
||||
name: 'Credit Card',
|
||||
debitsPositive: false,
|
||||
}
|
||||
public static readonly BROKERAGE: AccountType = {
|
||||
id: 'BROKERAGE',
|
||||
name: 'Brokerage',
|
||||
debitsPositive: true,
|
||||
}
|
||||
|
||||
public static of(id: string): AccountType {
|
||||
if (id === 'CHECKING') return AccountTypes.CHECKING
|
||||
if (id === 'SAVINGS') return AccountTypes.SAVINGS
|
||||
if (id === 'CREDIT_CARD') return AccountTypes.CREDIT_CARD
|
||||
if (id === 'BROKERAGE') return AccountTypes.BROKERAGE
|
||||
throw new Error('Unknown account type: ' + id)
|
||||
}
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
id: number
|
||||
createdAt: string
|
||||
|
@ -40,6 +77,10 @@ export class AccountApiClient extends ApiClient {
|
|||
return super.postJson(this.path, data)
|
||||
}
|
||||
|
||||
async updateAccount(id: number, data: AccountCreationPayload): Promise<Account> {
|
||||
return super.putJson(this.path + '/' + id, data)
|
||||
}
|
||||
|
||||
async deleteAccount(id: number): Promise<void> {
|
||||
return super.delete(this.path + '/' + id)
|
||||
}
|
||||
|
|
|
@ -42,40 +42,9 @@ a:hover {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.app-page-container {
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0.5rem;
|
||||
padding-bottom: 1rem;
|
||||
background-color: var(--bg-page);
|
||||
|
||||
border-bottom-left-radius: 2rem;
|
||||
border-bottom-right-radius: 2rem;
|
||||
}
|
||||
|
||||
.app-page-title {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.app-module-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.app-module {
|
||||
min-width: 300px;
|
||||
min-height: 200px;
|
||||
flex-grow: 1;
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.app-module-header {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
defineProps<{ buttonStyle?: string, icon?: string }>()
|
||||
defineProps<{
|
||||
buttonStyle?: string,
|
||||
icon?: string,
|
||||
buttonType?: "button" | "submit" | "reset" | undefined,
|
||||
disabled?: boolean
|
||||
}>()
|
||||
defineEmits(['click'])
|
||||
</script>
|
||||
<template>
|
||||
<button class="app-button" :class="{ 'app-button-secondary': buttonStyle === 'secondary' }" @click="$emit('click')">
|
||||
<button class="app-button" :class="{ 'app-button-secondary': buttonStyle === 'secondary' }" @click="$emit('click')"
|
||||
:type="buttonType" :disabled="disabled ?? false">
|
||||
<span v-if="icon">
|
||||
<font-awesome-icon :icon="'fa-' + icon" style="margin-right: 0.5rem; margin-left: -0.5rem;"></font-awesome-icon>
|
||||
</span>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{ title: string }>()
|
||||
</script>
|
||||
<template>
|
||||
<div class="app-page">
|
||||
<h1 class="app-page-title">{{ title }}</h1>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="css">
|
||||
.app-page {
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0.5rem;
|
||||
padding-bottom: 1rem;
|
||||
background-color: var(--bg-page);
|
||||
|
||||
border-bottom-left-radius: 2rem;
|
||||
border-bottom-right-radius: 2rem;
|
||||
}
|
||||
|
||||
.app-page-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue';
|
||||
import ModalWrapper from './ModalWrapper.vue';
|
||||
import AppButton from './AppButton.vue';
|
||||
|
||||
const globalAlertModal = useTemplateRef('global-alert-modal')
|
||||
</script>
|
||||
<template>
|
||||
<ModalWrapper id="global-alert-modal" ref="global-alert-modal">
|
||||
<template v-slot:default>
|
||||
<p id="global-alert-modal-text">This is an alert!</p>
|
||||
</template>
|
||||
<template v-slot:buttons>
|
||||
<AppButton id="global-alert-modal-ok-button" @click="globalAlertModal?.close('ok')">Ok</AppButton>
|
||||
<AppButton id="global-alert-modal-close-button" button-style="secondary"
|
||||
@click="globalAlertModal?.close('close')">Close</AppButton>
|
||||
<AppButton id="global-alert-modal-cancel-button" button-style="secondary"
|
||||
@click="globalAlertModal?.close('cancel')">Cancel</AppButton>
|
||||
</template>
|
||||
</ModalWrapper>
|
||||
</template>
|
|
@ -0,0 +1,35 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{ title: string }>()
|
||||
</script>
|
||||
<template>
|
||||
<div class="app-module">
|
||||
<div>
|
||||
<h2 class="app-module-header">{{ title }}</h2>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="app-module-actions">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="css">
|
||||
.app-module {
|
||||
min-width: 300px;
|
||||
min-height: 200px;
|
||||
flex-grow: 1;
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.app-module-header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-module-actions {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
|
@ -29,7 +29,7 @@ defineExpose({ show, close })
|
|||
|
||||
<div class="app-modal-dialog-actions">
|
||||
<slot name="buttons">
|
||||
<AppButton style="secondary" @click="close()">Close</AppButton>
|
||||
<AppButton button-style="secondary" @click="close()">Close</AppButton>
|
||||
</slot>
|
||||
</div>
|
||||
</dialog>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
<template>
|
||||
<table class="app-properties-table">
|
||||
<tbody>
|
||||
<slot></slot>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<style lang="css">
|
||||
.app-properties-table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.app-properties-table th {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
padding-right: 5px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
defineEmits<{ 'submit': void }>()
|
||||
</script>
|
||||
<template>
|
||||
<form @submit.prevent="e => $emit('submit')">
|
||||
<slot></slot>
|
||||
</form>
|
||||
</template>
|
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import AppButton from '../AppButton.vue';
|
||||
|
||||
defineEmits<{ 'cancel': void }>()
|
||||
defineProps<{ submitText?: string, cancelText?: string, disabled?: boolean }>()
|
||||
</script>
|
||||
<template>
|
||||
<div class="app-form-actions">
|
||||
<AppButton button-type="submit" :disabled="disabled ?? false">{{ submitText ?? 'Submit' }}</AppButton>
|
||||
<AppButton button-style="secondary" @click="$emit('cancel')" :disabled="disabled ?? false">{{ cancelText ?? 'Cancel'
|
||||
}}</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="css">
|
||||
.app-form-actions {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,26 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
}>()
|
||||
</script>
|
||||
<template>
|
||||
<div class="app-form-control">
|
||||
<label>
|
||||
{{ label }}
|
||||
<slot></slot>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="css">
|
||||
.app-form-control {
|
||||
flex-grow: 1;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.app-form-control>label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
<template>
|
||||
<div class="app-form-group">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="css">
|
||||
.app-form-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,88 @@
|
|||
<script setup lang="ts">
|
||||
import { AccountApiClient, type Account } from '@/api/account';
|
||||
import AppButton from '@/components/AppButton.vue';
|
||||
import AppPage from '@/components/AppPage.vue';
|
||||
import PropertiesTable from '@/components/PropertiesTable.vue';
|
||||
import { useProfileStore } from '@/stores/profile-store';
|
||||
import { showConfirm } from '@/util/alert';
|
||||
import { onMounted, ref, type Ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const profileStore = useProfileStore()
|
||||
const account: Ref<Account | null> = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
if (!profileStore.state) {
|
||||
await router.replace('/')
|
||||
return
|
||||
}
|
||||
|
||||
const accountId = parseInt(route.params.id as string)
|
||||
try {
|
||||
const api = new AccountApiClient(profileStore.state)
|
||||
account.value = await api.getAccount(accountId)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
await router.replace('/')
|
||||
}
|
||||
})
|
||||
|
||||
async function deleteAccount() {
|
||||
if (!profileStore.state || !account.value) return
|
||||
if (await showConfirm('Are you sure you want to delete this account? This will permanently remove the account and all associated transactions.')) {
|
||||
try {
|
||||
const api = new AccountApiClient(profileStore.state)
|
||||
await api.deleteAccount(account.value.id)
|
||||
await router.replace('/')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<AppPage :title="account?.name ?? ''">
|
||||
<PropertiesTable v-if="account">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<td>{{ account.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created at</th>
|
||||
<td>{{ new Date(account.createdAt).toLocaleString() }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Archived</th>
|
||||
<td>{{ account.archived }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<td>{{ account.type }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td>{{ account.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Number Suffix</th>
|
||||
<td>{{ account.numberSuffix }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Currency</th>
|
||||
<td>{{ account.currency }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<td>{{ account.description }}</td>
|
||||
</tr>
|
||||
</PropertiesTable>
|
||||
<div>
|
||||
<AppButton icon="wrench"
|
||||
@click="router.push(`/profiles/${profileStore.state?.name}/accounts/${account?.id}/edit`)">Edit</AppButton>
|
||||
<AppButton icon="trash" @click="deleteAccount()">Delete</AppButton>
|
||||
</div>
|
||||
</AppPage>
|
||||
</template>
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ProfileApiClient, type Profile } from '@/api/profile';
|
||||
import AppButton from '@/components/AppButton.vue';
|
||||
import AppPage from '@/components/AppPage.vue';
|
||||
import ModalWrapper from '@/components/ModalWrapper.vue';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useProfileStore } from '@/stores/profile-store';
|
||||
|
@ -51,9 +52,7 @@ async function addProfile() {
|
|||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="app-page-container">
|
||||
<h1 class="app-page-title">Select a Profile</h1>
|
||||
|
||||
<AppPage title="Select a Profile">
|
||||
<div class="profile-card" v-for="profile in profiles" :key="profile.name" @click="selectProfile(profile)">
|
||||
<span>{{ profile.name }}</span>
|
||||
</div>
|
||||
|
@ -74,7 +73,7 @@ async function addProfile() {
|
|||
<AppButton button-style="secondary" @click="addProfileModal?.close()">Cancel</AppButton>
|
||||
</template>
|
||||
</ModalWrapper>
|
||||
</div>
|
||||
</AppPage>
|
||||
</template>
|
||||
<style lang="css">
|
||||
.profile-card {
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
<script setup lang="ts">
|
||||
import { AccountApiClient, AccountTypes, type Account, type AccountType } from '@/api/account';
|
||||
import AppPage from '@/components/AppPage.vue';
|
||||
import AppForm from '@/components/form/AppForm.vue';
|
||||
import FormActions from '@/components/form/FormActions.vue';
|
||||
import FormControl from '@/components/form/FormControl.vue';
|
||||
import FormGroup from '@/components/form/FormGroup.vue';
|
||||
import { useProfileStore } from '@/stores/profile-store';
|
||||
import { computed, onMounted, ref, type Ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const profileStore = useProfileStore()
|
||||
|
||||
const existingAccount: Ref<Account | null> = ref(null)
|
||||
const editing = computed(() => {
|
||||
return existingAccount.value !== null || route.meta.title === 'Edit Account'
|
||||
})
|
||||
const loading = ref(false)
|
||||
|
||||
const accountName = ref('')
|
||||
const accountType: Ref<AccountType> = ref(AccountTypes.CHECKING)
|
||||
const accountNumberSuffix = ref('')
|
||||
const currency = ref('USD')
|
||||
const description = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
console.log(route)
|
||||
if (!profileStore.state) return
|
||||
|
||||
const accountIdStr = route.params.id
|
||||
if (accountIdStr && typeof (accountIdStr) === 'string') {
|
||||
const accountId = parseInt(accountIdStr)
|
||||
try {
|
||||
loading.value = true
|
||||
const api = new AccountApiClient(profileStore.state)
|
||||
existingAccount.value = await api.getAccount(accountId)
|
||||
accountName.value = existingAccount.value.name
|
||||
accountType.value = AccountTypes.of(existingAccount.value.type)
|
||||
accountNumberSuffix.value = existingAccount.value.numberSuffix
|
||||
currency.value = existingAccount.value.currency
|
||||
description.value = existingAccount.value.description
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function doSubmit() {
|
||||
if (!profileStore.state) return
|
||||
const payload = {
|
||||
name: accountName.value,
|
||||
description: description.value,
|
||||
type: accountType.value.id,
|
||||
currency: currency.value,
|
||||
numberSuffix: accountNumberSuffix.value
|
||||
}
|
||||
|
||||
try {
|
||||
const api = new AccountApiClient(profileStore.state)
|
||||
loading.value = true
|
||||
const account = editing.value
|
||||
? await api.updateAccount(existingAccount.value?.id ?? 0, payload)
|
||||
: await api.createAccount(payload)
|
||||
await router.replace(`/profiles/${profileStore.state.name}/accounts/${account.id}`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<AppPage :title="editing ? 'Edit Account' : 'Add Account'">
|
||||
<AppForm @submit="doSubmit()">
|
||||
<FormGroup>
|
||||
<FormControl label="Account Name" style="max-width: 200px;">
|
||||
<input v-model="accountName" :disabled="loading" />
|
||||
</FormControl>
|
||||
<FormControl label="Account Type">
|
||||
<select id="account-type-select" v-model="accountType" :disabled="loading" required>
|
||||
<option :value="AccountTypes.CHECKING">{{ AccountTypes.CHECKING.name }}</option>
|
||||
<option :value="AccountTypes.SAVINGS">{{ AccountTypes.SAVINGS.name }}</option>
|
||||
<option :value="AccountTypes.CREDIT_CARD">{{ AccountTypes.CREDIT_CARD.name }}</option>
|
||||
<option :value="AccountTypes.BROKERAGE">{{ AccountTypes.BROKERAGE.name }}</option>
|
||||
</select>
|
||||
</FormControl>
|
||||
<FormControl label="Currency">
|
||||
<select id="currency-select" v-model="currency" :disabled="loading" required>
|
||||
<option value="USD">USD</option>
|
||||
<option value="EUR">EUR</option>
|
||||
<option value="GBP">GBP</option>
|
||||
</select>
|
||||
</FormControl>
|
||||
<FormControl label="Account Number Suffix" style="max-width: 200px;">
|
||||
<input id="account-number-suffix-input" v-model="accountNumberSuffix" minlength="4" maxlength="4"
|
||||
:disabled="loading" required />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormControl label="Description">
|
||||
<textarea id="description-textarea" v-model="description" :disabled="loading"></textarea>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormActions @cancel="router.replace('/')" :disabled="loading" :submit-text="editing ? 'Save' : 'Add'" />
|
||||
</AppForm>
|
||||
</AppPage>
|
||||
</template>
|
|
@ -1,8 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { AccountApiClient, type Account } from '@/api/account'
|
||||
import AppButton from '@/components/AppButton.vue'
|
||||
import HomeModule from '@/components/HomeModule.vue'
|
||||
import { useProfileStore } from '@/stores/profile-store'
|
||||
import { onMounted, ref, type Ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const profileStore = useProfileStore()
|
||||
|
||||
const accounts: Ref<Account[]> = ref([])
|
||||
|
@ -19,8 +23,8 @@ onMounted(async () => {
|
|||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="app-module">
|
||||
<h2 class="app-module-header">Accounts</h2>
|
||||
<HomeModule title="Accounts">
|
||||
<template v-slot:default>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -32,13 +36,21 @@ onMounted(async () => {
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="account in accounts" :key="account.id">
|
||||
<td>{{ account.name }}</td>
|
||||
<td>
|
||||
<RouterLink :to="`/profiles/${profileStore.state?.name}/accounts/${account.id}`">{{ account.name }}
|
||||
</RouterLink>
|
||||
</td>
|
||||
<td>{{ account.currency }}</td>
|
||||
<td>...{{ account.numberSuffix }}</td>
|
||||
<td>{{ account.type }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<AppButton icon="plus" @click="router.push(`/profiles/${profileStore.state?.name}/add-account`)">Add Account
|
||||
</AppButton>
|
||||
</template>
|
||||
</HomeModule>
|
||||
</template>
|
||||
<style lang="css"></style>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { ProfileApiClient } from '@/api/profile';
|
||||
import AppButton from '@/components/AppButton.vue';
|
||||
import ConfirmModal from '@/components/ConfirmModal.vue';
|
||||
import HomeModule from '@/components/HomeModule.vue';
|
||||
import { useProfileStore } from '@/stores/profile-store';
|
||||
import { useTemplateRef } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
@ -23,20 +24,18 @@ async function deleteProfile() {
|
|||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="app-module" style="display: flex; flex-direction: column; justify-content: space-between;">
|
||||
<div>
|
||||
<h2 class="app-module-header">Profile</h2>
|
||||
<HomeModule title="Profile">
|
||||
<template v-slot:default>
|
||||
<p>Your currently selected profile is: {{ profileStore.state?.name }}</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: right;">
|
||||
<AppButton icon="folder-open" @click="router.push('/profiles')">Choose another profile</AppButton>
|
||||
<AppButton button-style="secondary" icon="trash" @click="deleteProfile()">Delete</AppButton>
|
||||
</div>
|
||||
|
||||
<ConfirmModal ref="confirmDeleteModal">
|
||||
<p>Are you sure you want to delete this profile?</p>
|
||||
<p>This will permanently remove all data associated with this profile, and this cannot be undone!</p>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<AppButton icon="folder-open" @click="router.push('/profiles')">Choose another profile</AppButton>
|
||||
<AppButton button-style="secondary" icon="trash" @click="deleteProfile()">Delete</AppButton>
|
||||
</template>
|
||||
</HomeModule>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { Page, PageRequest } from '@/api/pagination';
|
||||
import { TransactionApiClient, type Transaction } from '@/api/transaction';
|
||||
import HomeModule from '@/components/HomeModule.vue';
|
||||
import PaginationControls from '@/components/PaginationControls.vue';
|
||||
import { useProfileStore } from '@/stores/profile-store';
|
||||
import { onMounted, ref, type Ref } from 'vue';
|
||||
|
@ -23,8 +24,8 @@ async function fetchPage(pageRequest: PageRequest) {
|
|||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="app-module">
|
||||
<h2 class="app-module-header">Transactions</h2>
|
||||
<HomeModule title="Transactions">
|
||||
<template v-slot:default>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -45,6 +46,7 @@ async function fetchPage(pageRequest: PageRequest) {
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<PaginationControls :page="transactions" @update="pr => fetchPage(pr)" />
|
||||
</div>
|
||||
<PaginationControls :page="transactions" @update="pr => fetchPage(pr)"></PaginationControls>
|
||||
</template>
|
||||
</HomeModule>
|
||||
</template>
|
||||
|
|
|
@ -6,6 +6,8 @@ import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vu
|
|||
import UserHomePage from '@/pages/UserHomePage.vue'
|
||||
import ProfilesPage from '@/pages/ProfilesPage.vue'
|
||||
import { useProfileStore } from '@/stores/profile-store'
|
||||
import AccountPage from '@/pages/AccountPage.vue'
|
||||
import EditAccountPage from '@/pages/forms/EditAccountPage.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
@ -32,10 +34,31 @@ const router = createRouter({
|
|||
meta: { title: 'Profiles' },
|
||||
},
|
||||
{
|
||||
path: '/profiles/:name',
|
||||
path: 'profiles/:name',
|
||||
beforeEnter: profileSelected,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: async () => ProfilePage,
|
||||
meta: { title: (to: RouteLocationNormalized) => 'Profile ' + to.params.name },
|
||||
},
|
||||
{
|
||||
path: 'accounts/:id',
|
||||
component: async () => AccountPage,
|
||||
meta: { title: 'Account' },
|
||||
},
|
||||
{
|
||||
path: 'accounts/:id/edit',
|
||||
component: async () => EditAccountPage,
|
||||
meta: { title: 'Edit Account' },
|
||||
},
|
||||
{
|
||||
path: 'add-account',
|
||||
component: async () => EditAccountPage,
|
||||
meta: { title: 'Add Account' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
export function showAlert(message: string): Promise<void> {
|
||||
const modal: HTMLDialogElement = document.getElementById(
|
||||
'global-alert-modal',
|
||||
) as HTMLDialogElement
|
||||
const modalText: HTMLParagraphElement = document.getElementById(
|
||||
'global-alert-modal-text',
|
||||
) as HTMLParagraphElement
|
||||
|
||||
modalText.innerText = message
|
||||
getText().innerText = message
|
||||
getOkButton().style.display = 'none'
|
||||
getCancelButton().style.display = 'none'
|
||||
getCloseButton().style.display = ''
|
||||
return new Promise((resolve) => {
|
||||
const modal = getModal()
|
||||
modal.showModal()
|
||||
modal.addEventListener(
|
||||
'close',
|
||||
|
@ -18,3 +15,35 @@ export function showAlert(message: string): Promise<void> {
|
|||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function showConfirm(message: string): Promise<boolean> {
|
||||
getText().innerText = message
|
||||
getOkButton().style.display = ''
|
||||
getCancelButton().style.display = ''
|
||||
getCloseButton().style.display = 'none'
|
||||
return new Promise((resolve) => {
|
||||
const modal = getModal()
|
||||
modal.showModal()
|
||||
modal.addEventListener('close', () => resolve(modal.returnValue === 'ok'), { once: true })
|
||||
})
|
||||
}
|
||||
|
||||
function getModal(): HTMLDialogElement {
|
||||
return document.getElementById('global-alert-modal') as HTMLDialogElement
|
||||
}
|
||||
|
||||
function getText(): HTMLParagraphElement {
|
||||
return document.getElementById('global-alert-modal-text') as HTMLParagraphElement
|
||||
}
|
||||
|
||||
function getOkButton(): HTMLButtonElement {
|
||||
return document.getElementById('global-alert-modal-ok-button') as HTMLButtonElement
|
||||
}
|
||||
|
||||
function getCancelButton(): HTMLButtonElement {
|
||||
return document.getElementById('global-alert-modal-cancel-button') as HTMLButtonElement
|
||||
}
|
||||
|
||||
function getCloseButton(): HTMLButtonElement {
|
||||
return document.getElementById('global-alert-modal-close-button') as HTMLButtonElement
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue