Value record page, improved history, link styling, etc.
Build and Deploy Web App / build-and-deploy (push) Successful in 19s
Details
Build and Deploy Web App / build-and-deploy (push) Successful in 19s
Details
This commit is contained in:
parent
41acb0dd51
commit
30764ba624
|
|
@ -3,6 +3,7 @@ import { ApiClient } from './base'
|
||||||
import type { Currency } from './data'
|
import type { Currency } from './data'
|
||||||
import type { Page, PageRequest } from './pagination'
|
import type { Page, PageRequest } from './pagination'
|
||||||
import { getSelectedProfile } from './profile'
|
import { getSelectedProfile } from './profile'
|
||||||
|
import type { Attachment } from './attachment'
|
||||||
|
|
||||||
export interface AccountType {
|
export interface AccountType {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -87,6 +88,7 @@ export interface AccountValueRecord {
|
||||||
type: AccountValueRecordType
|
type: AccountValueRecordType
|
||||||
value: number
|
value: number
|
||||||
currency: Currency
|
currency: Currency
|
||||||
|
attachments: Attachment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccountValueRecordCreationPayload {
|
export interface AccountValueRecordCreationPayload {
|
||||||
|
|
@ -103,6 +105,14 @@ export enum AccountHistoryItemType {
|
||||||
JOURNAL_ENTRY = 'JOURNAL_ENTRY',
|
JOURNAL_ENTRY = 'JOURNAL_ENTRY',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function accountHistoryItemTypeToDisplayName(type: AccountHistoryItemType) {
|
||||||
|
if (type === AccountHistoryItemType.TEXT) return 'Text'
|
||||||
|
if (type === AccountHistoryItemType.PROPERTY_CHANGE) return 'Property Change'
|
||||||
|
if (type === AccountHistoryItemType.VALUE_RECORD) return 'Value Record'
|
||||||
|
if (type === AccountHistoryItemType.JOURNAL_ENTRY) return 'Journal Entry'
|
||||||
|
return 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
export interface AccountHistoryItem {
|
export interface AccountHistoryItem {
|
||||||
timestamp: string
|
timestamp: string
|
||||||
type: AccountHistoryItemType
|
type: AccountHistoryItemType
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,14 @@ import type { RouteLocation } from 'vue-router'
|
||||||
import { ApiClient } from './base'
|
import { ApiClient } from './base'
|
||||||
import { getSelectedProfile } from './profile'
|
import { getSelectedProfile } from './profile'
|
||||||
|
|
||||||
|
export interface Attachment {
|
||||||
|
id: number
|
||||||
|
uploadedAt: string
|
||||||
|
filename: string
|
||||||
|
contentType: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
export class AttachmentApiClient extends ApiClient {
|
export class AttachmentApiClient extends ApiClient {
|
||||||
readonly profileName: string
|
readonly profileName: string
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { Attachment } from './attachment'
|
||||||
import { ApiClient } from './base'
|
import { ApiClient } from './base'
|
||||||
import type { Currency } from './data'
|
import type { Currency } from './data'
|
||||||
import { type Page, type PageRequest } from './pagination'
|
import { type Page, type PageRequest } from './pagination'
|
||||||
|
|
@ -86,7 +87,7 @@ export interface TransactionDetail {
|
||||||
debitedAccount: TransactionDetailAccount | null
|
debitedAccount: TransactionDetailAccount | null
|
||||||
tags: string[]
|
tags: string[]
|
||||||
lineItems: TransactionDetailLineItem[]
|
lineItems: TransactionDetailLineItem[]
|
||||||
attachments: TransactionDetailAttachment[]
|
attachments: Attachment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionDetailAccount {
|
export interface TransactionDetailAccount {
|
||||||
|
|
@ -104,14 +105,6 @@ export interface TransactionDetailLineItem {
|
||||||
category: TransactionCategory | null
|
category: TransactionCategory | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionDetailAttachment {
|
|
||||||
id: number
|
|
||||||
uploadedAt: string
|
|
||||||
filename: string
|
|
||||||
contentType: string
|
|
||||||
size: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AddTransactionPayload {
|
export interface AddTransactionPayload {
|
||||||
timestamp: string
|
timestamp: string
|
||||||
amount: number
|
amount: number
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
@import url('@/assets/styles/fonts.css');
|
@import url('@/assets/styles/fonts.css');
|
||||||
@import url('@/assets/styles/text.css');
|
@import url('@/assets/styles/text.css');
|
||||||
|
@import url('@/assets/styles/spacing.css');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--theme-primary: #113188;
|
--theme-primary: #113188;
|
||||||
|
|
@ -12,6 +13,7 @@
|
||||||
|
|
||||||
--text: rgb(247, 247, 247);
|
--text: rgb(247, 247, 247);
|
||||||
--text-muted: gray;
|
--text-muted: gray;
|
||||||
|
--text-link: #6c8eff;
|
||||||
--positive: rgb(59, 219, 44);
|
--positive: rgb(59, 219, 44);
|
||||||
--negative: rgb(253, 52, 52);
|
--negative: rgb(253, 52, 52);
|
||||||
--warning: rgb(255, 187, 0);
|
--warning: rgb(255, 187, 0);
|
||||||
|
|
@ -26,11 +28,11 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--theme-primary);
|
color: var(--text-link);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--theme-tertiary);
|
color: var(--text-tertiary);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
.p0 {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m0 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-1 {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
@ -41,3 +41,7 @@
|
||||||
.align-left {
|
.align-left {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { AccountApiClient, AccountValueRecordType, type Account, type AccountVal
|
||||||
import { datetimeLocalToISO, getDatetimeLocalValueForNow } from '@/util/time';
|
import { datetimeLocalToISO, getDatetimeLocalValueForNow } from '@/util/time';
|
||||||
import FileSelector from '@/components/common/FileSelector.vue';
|
import FileSelector from '@/components/common/FileSelector.vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
import { showConfirm } from '@/util/alert';
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const props = defineProps<{ account: Account }>()
|
const props = defineProps<{ account: Account }>()
|
||||||
|
|
@ -23,7 +24,11 @@ const attachments: Ref<File[]> = ref([])
|
||||||
async function show(): Promise<AccountValueRecord | undefined> {
|
async function show(): Promise<AccountValueRecord | undefined> {
|
||||||
if (!modal.value) return Promise.resolve(undefined)
|
if (!modal.value) return Promise.resolve(undefined)
|
||||||
timestamp.value = getDatetimeLocalValueForNow()
|
timestamp.value = getDatetimeLocalValueForNow()
|
||||||
amount.value = props.account.currentBalance ?? 0
|
if (props.account.currentBalance !== null) {
|
||||||
|
amount.value = props.account.currentBalance / Math.pow(10, props.account.currency.fractionalDigits)
|
||||||
|
} else {
|
||||||
|
amount.value = 0
|
||||||
|
}
|
||||||
savedValueRecord.value = undefined
|
savedValueRecord.value = undefined
|
||||||
const result = await modal.value.show()
|
const result = await modal.value.show()
|
||||||
if (result === 'saved') {
|
if (result === 'saved') {
|
||||||
|
|
@ -38,6 +43,11 @@ async function addValueRecord() {
|
||||||
type: AccountValueRecordType.BALANCE,
|
type: AccountValueRecordType.BALANCE,
|
||||||
value: Math.round(amount.value * Math.pow(10, props.account.currency.fractionalDigits))
|
value: Math.round(amount.value * Math.pow(10, props.account.currency.fractionalDigits))
|
||||||
}
|
}
|
||||||
|
// Check and confirm with the user if the value they entered doesn't match the expected balance of the account.
|
||||||
|
if (props.account.currentBalance !== null && payload.value !== props.account.currentBalance) {
|
||||||
|
const result = await showConfirm("The balance you entered doesn't match the expected current balance for this account. Proceeding to add this value record will result in an incomplete account history and possible reconciliation errors. Are you sure you want to proceed?")
|
||||||
|
if (!result) return
|
||||||
|
}
|
||||||
const api = new AccountApiClient(route)
|
const api = new AccountApiClient(route)
|
||||||
try {
|
try {
|
||||||
savedValueRecord.value = await api.createValueRecord(props.account.id, payload, attachments.value)
|
savedValueRecord.value = await api.createValueRecord(props.account.id, payload, attachments.value)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { AttachmentApiClient } from '@/api/attachment';
|
||||||
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import AppButton from './AppButton.vue';
|
||||||
|
|
||||||
|
|
||||||
|
interface AttachmentInfo {
|
||||||
|
filename: string
|
||||||
|
contentType: string
|
||||||
|
size: number
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const props = defineProps<{ attachment: AttachmentInfo, disabled?: boolean }>()
|
||||||
|
defineEmits<{ "deleted": void }>()
|
||||||
|
|
||||||
|
function downloadFile() {
|
||||||
|
const api = new AttachmentApiClient(route)
|
||||||
|
if (!authStore.state) return
|
||||||
|
api.downloadAttachment(props.attachment?.id ?? 0, authStore.state.token)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="attachment-row">
|
||||||
|
<div style="display: flex; align-items: center; margin-left: 1rem;">
|
||||||
|
<div>
|
||||||
|
<div>{{ attachment.filename }}</div>
|
||||||
|
<div style="font-size: 0.75rem;">{{ attachment.contentType }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<AppButton icon="download" type="button" @click="downloadFile()" v-if="attachment.id !== undefined" />
|
||||||
|
<AppButton v-if="!disabled" type="button" icon="trash" @click="$emit('deleted')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="css">
|
||||||
|
.attachment-row {
|
||||||
|
background-color: var(--bg);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, useTemplateRef, watch, type Ref } from 'vue';
|
import { onMounted, ref, useTemplateRef, watch, type Ref } from 'vue';
|
||||||
import AppButton from '@/components/common/AppButton.vue';
|
import AppButton from '@/components/common/AppButton.vue';
|
||||||
import { AttachmentApiClient } from '@/api/attachment';
|
import AttachmentRow from './AttachmentRow.vue';
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useAuthStore } from '@/stores/auth-store';
|
|
||||||
|
|
||||||
interface ExistingFile {
|
interface ExistingFile {
|
||||||
id: number
|
id: number
|
||||||
|
|
@ -41,8 +39,6 @@ interface Props {
|
||||||
initialFiles?: ExistingFile[]
|
initialFiles?: ExistingFile[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
initialFiles: () => []
|
initialFiles: () => []
|
||||||
|
|
@ -105,28 +101,11 @@ function onFileDeleteClicked(idx: number) {
|
||||||
}
|
}
|
||||||
files.value.splice(idx, 1)
|
files.value.splice(idx, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadFile(attachmentId: number) {
|
|
||||||
const api = new AttachmentApiClient(route)
|
|
||||||
if (!authStore.state) return
|
|
||||||
api.downloadAttachment(attachmentId, authStore.state.token)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="file-selector">
|
<div class="file-selector">
|
||||||
<div @click.prevent="">
|
<div @click.prevent="">
|
||||||
<div v-for="file, idx in files" :key="idx" class="file-selector-item">
|
<AttachmentRow v-for="file, idx in files" :key="idx" :attachment="file" @deleted="onFileDeleteClicked(idx)" />
|
||||||
<div style="display: flex; align-items: center; margin-left: 1rem;">
|
|
||||||
<div>
|
|
||||||
<div>{{ file.filename }}</div>
|
|
||||||
<div style="font-size: 0.75rem;">{{ file.contentType }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<AppButton icon="download" @click="downloadFile(file.id)" v-if="(file instanceof ExistingFileListItem)" />
|
|
||||||
<AppButton v-if="!disabled" icon="trash" @click="onFileDeleteClicked(idx)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -143,13 +122,4 @@ function downloadFile(attachmentId: number) {
|
||||||
.file-selector {
|
.file-selector {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-selector-item {
|
|
||||||
background-color: var(--bg-secondary);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.25rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -5,28 +5,49 @@ import { onMounted, ref, type Ref } from 'vue';
|
||||||
import ValueRecordHistoryItem from './ValueRecordHistoryItem.vue';
|
import ValueRecordHistoryItem from './ValueRecordHistoryItem.vue';
|
||||||
import JournalEntryHistoryItem from './JournalEntryHistoryItem.vue';
|
import JournalEntryHistoryItem from './JournalEntryHistoryItem.vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
import AppButton from '../common/AppButton.vue';
|
||||||
|
import AppBadge from '../common/AppBadge.vue';
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const props = defineProps<{ accountId: number }>()
|
const props = defineProps<{ accountId: number }>()
|
||||||
const historyItems: Ref<AccountHistoryItem[]> = ref([])
|
const historyItems: Ref<AccountHistoryItem[]> = ref([])
|
||||||
|
const canLoadMore = ref(true)
|
||||||
|
const nextPage: Ref<PageRequest> = ref({ page: 1, size: 10, sorts: [{ attribute: 'timestamp', dir: 'DESC' }] })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const pageRequest: PageRequest = { page: 1, size: 10, sorts: [{ attribute: 'timestamp', dir: 'DESC' }] }
|
await loadNextPage()
|
||||||
const api = new AccountApiClient(route)
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
const page = await api.getHistory(props.accountId, pageRequest)
|
|
||||||
historyItems.value.push(...page.items)
|
|
||||||
if (page.isLast) return
|
|
||||||
pageRequest.page++
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
historyItems.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function loadNextPage() {
|
||||||
|
const api = new AccountApiClient(route)
|
||||||
|
try {
|
||||||
|
const page = await api.getHistory(props.accountId, nextPage.value)
|
||||||
|
historyItems.value.push(...page.items)
|
||||||
|
canLoadMore.value = !page.isLast
|
||||||
|
if (canLoadMore.value) {
|
||||||
|
nextPage.value.page++
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
historyItems.value = []
|
||||||
|
canLoadMore.value = false
|
||||||
|
nextPage.value.page = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAll() {
|
||||||
|
while (canLoadMore.value) {
|
||||||
|
await loadNextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
nextPage.value.page = 1
|
||||||
|
canLoadMore.value = true
|
||||||
|
historyItems.value = []
|
||||||
|
loadNextPage()
|
||||||
|
}
|
||||||
|
|
||||||
function asVR(i: AccountHistoryItem): AccountHistoryValueRecordItem {
|
function asVR(i: AccountHistoryItem): AccountHistoryValueRecordItem {
|
||||||
return i as AccountHistoryValueRecordItem
|
return i as AccountHistoryValueRecordItem
|
||||||
}
|
}
|
||||||
|
|
@ -34,19 +55,27 @@ function asVR(i: AccountHistoryItem): AccountHistoryValueRecordItem {
|
||||||
function asJE(i: AccountHistoryItem): AccountHistoryJournalEntryItem {
|
function asJE(i: AccountHistoryItem): AccountHistoryJournalEntryItem {
|
||||||
return i as AccountHistoryJournalEntryItem
|
return i as AccountHistoryJournalEntryItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({ reload })
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-for="item in historyItems" :key="item.timestamp" class="history-item">
|
<div v-for="item in historyItems" :key="item.timestamp" class="history-item">
|
||||||
<div class="history-item-header">
|
<div class="history-item-header">
|
||||||
<div class="font-mono font-size-xsmall">{{ new Date(item.timestamp).toLocaleString() }}</div>
|
<div class="font-mono font-size-xsmall text-muted">{{ new Date(item.timestamp).toLocaleString() }}</div>
|
||||||
<div>{{ item.type }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ValueRecordHistoryItem v-if="item.type === AccountHistoryItemType.VALUE_RECORD" :item="asVR(item)"
|
<ValueRecordHistoryItem v-if="item.type === AccountHistoryItemType.VALUE_RECORD" :item="asVR(item)"
|
||||||
:account-id="accountId" />
|
:account-id="accountId" />
|
||||||
|
|
||||||
<JournalEntryHistoryItem v-if="item.type === AccountHistoryItemType.JOURNAL_ENTRY" :item="asJE(item)" />
|
<JournalEntryHistoryItem v-if="item.type === AccountHistoryItemType.JOURNAL_ENTRY" :item="asJE(item)" />
|
||||||
|
|
||||||
|
<hr class="m0" />
|
||||||
|
</div>
|
||||||
|
<div class="align-center">
|
||||||
|
<AppButton size="md" @click="loadNextPage()" v-if="canLoadMore">Load more history...</AppButton>
|
||||||
|
<AppButton size="sm" @click="loadAll()" theme="secondary" v-if="canLoadMore">Load all</AppButton>
|
||||||
|
<AppBadge v-if="!canLoadMore">This is the start of this account's history.</AppBadge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { AccountHistoryJournalEntryItem } from '@/api/account'
|
||||||
import { formatMoney } from '@/api/data';
|
import { formatMoney } from '@/api/data';
|
||||||
import { getSelectedProfile } from '@/api/profile';
|
import { getSelectedProfile } from '@/api/profile';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
import AppBadge from '../common/AppBadge.vue';
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
|
@ -17,9 +18,11 @@ defineProps<{ item: AccountHistoryJournalEntryItem }>()
|
||||||
entered as a
|
entered as a
|
||||||
<strong>{{ item.journalEntryType.toLowerCase() }}</strong>
|
<strong>{{ item.journalEntryType.toLowerCase() }}</strong>
|
||||||
for this account with a value of
|
for this account with a value of
|
||||||
{{ formatMoney(item.amount, item.currency) }}.
|
<AppBadge class="font-mono">{{ formatMoney(item.amount, item.currency) }}</AppBadge>
|
||||||
<br />
|
<br />
|
||||||
{{ item.transactionDescription }}
|
<p class="font-size-small m0 mt-1">
|
||||||
|
{{ item.transactionDescription }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,26 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AccountApiClient, type AccountHistoryValueRecordItem } from '@/api/account';
|
import { type AccountHistoryValueRecordItem } from '@/api/account';
|
||||||
import { formatMoney } from '@/api/data';
|
import { formatMoney } from '@/api/data';
|
||||||
import AppButton from '../common/AppButton.vue';
|
|
||||||
import { showConfirm } from '@/util/alert';
|
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import ButtonBar from '../common/ButtonBar.vue';
|
import AppBadge from '../common/AppBadge.vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { getSelectedProfile } from '@/api/profile';
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const props = defineProps<{ item: AccountHistoryValueRecordItem, accountId: number }>()
|
const props = defineProps<{ item: AccountHistoryValueRecordItem, accountId: number }>()
|
||||||
|
|
||||||
async function deleteValueRecord(id: number) {
|
const valueRecordRoute = computed(() => `/profiles/${getSelectedProfile(route)}/accounts/${props.accountId}/value-records/${props.item.valueRecordId}`)
|
||||||
const confirm = await showConfirm("Are you sure you want to delete this value record? This may affect how your account's balance is calculated.")
|
|
||||||
if (!confirm) return
|
|
||||||
const api = new AccountApiClient(route)
|
|
||||||
try {
|
|
||||||
await api.deleteValueRecord(props.accountId, id)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="history-item-content">
|
<div class="history-item-content">
|
||||||
<div>
|
<div>
|
||||||
Value recorded for this account at {{ formatMoney(item.value, item.currency) }}
|
<RouterLink :to="valueRecordRoute">Value recorded</RouterLink> as <AppBadge class="font-mono">{{
|
||||||
|
formatMoney(item.value,
|
||||||
|
item.currency) }}
|
||||||
|
</AppBadge>
|
||||||
</div>
|
</div>
|
||||||
<ButtonBar>
|
|
||||||
<AppButton type="button" icon="trash" size="sm" @click="deleteValueRecord(item.valueRecordId)">
|
|
||||||
Delete this record
|
|
||||||
</AppButton>
|
|
||||||
</ButtonBar>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const addValueRecordModal = useTemplateRef("addValueRecordModal")
|
const addValueRecordModal = useTemplateRef("addValueRecordModal")
|
||||||
|
const history = useTemplateRef('history')
|
||||||
const account: Ref<Account | null> = ref(null)
|
const account: Ref<Account | null> = ref(null)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
@ -45,7 +46,7 @@ async function deleteAccount() {
|
||||||
async function addValueRecord() {
|
async function addValueRecord() {
|
||||||
const result = await addValueRecordModal.value?.show()
|
const result = await addValueRecordModal.value?.show()
|
||||||
if (result) {
|
if (result) {
|
||||||
console.info('Value record added', result)
|
history.value?.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -97,7 +98,7 @@ async function addValueRecord() {
|
||||||
<AppButton icon="trash" @click="deleteAccount()">Delete</AppButton>
|
<AppButton icon="trash" @click="deleteAccount()">Delete</AppButton>
|
||||||
</ButtonBar>
|
</ButtonBar>
|
||||||
|
|
||||||
<AccountHistory :account-id="account.id" v-if="account" />
|
<AccountHistory :account-id="account.id" v-if="account" ref="history" />
|
||||||
|
|
||||||
<AddValueRecordModal v-if="account" :account="account" ref="addValueRecordModal" />
|
<AddValueRecordModal v-if="account" :account="account" ref="addValueRecordModal" />
|
||||||
</AppPage>
|
</AppPage>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import TagLabel from '@/components/TagLabel.vue';
|
||||||
import { showAlert, showConfirm } from '@/util/alert';
|
import { showAlert, showConfirm } from '@/util/alert';
|
||||||
import { onMounted, ref, type Ref } from 'vue';
|
import { onMounted, ref, type Ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import AttachmentRow from '@/components/common/AttachmentRow.vue';
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -117,6 +118,11 @@ async function deleteTransaction() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="transaction.attachments.length > 0">
|
||||||
|
<h3>Attachments</h3>
|
||||||
|
<AttachmentRow v-for="a in transaction.attachments" :attachment="a" :key="a.id" disabled />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<AppButton icon="wrench"
|
<AppButton icon="wrench"
|
||||||
@click="router.push(`/profiles/${getSelectedProfile(route)}/transactions/${transaction.id}/edit`)">
|
@click="router.push(`/profiles/${getSelectedProfile(route)}/transactions/${transaction.id}/edit`)">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { AccountApiClient, type AccountValueRecord } from '@/api/account';
|
||||||
|
import { formatMoney } from '@/api/data';
|
||||||
|
import { getSelectedProfile } from '@/api/profile';
|
||||||
|
import AppBadge from '@/components/common/AppBadge.vue';
|
||||||
|
import AppButton from '@/components/common/AppButton.vue';
|
||||||
|
import AppPage from '@/components/common/AppPage.vue';
|
||||||
|
import AttachmentRow from '@/components/common/AttachmentRow.vue';
|
||||||
|
import ButtonBar from '@/components/common/ButtonBar.vue';
|
||||||
|
import PropertiesTable from '@/components/PropertiesTable.vue';
|
||||||
|
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 valueRecord: Ref<AccountValueRecord | null> = ref(null)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const accountId = parseInt(route.params.accountId as string)
|
||||||
|
const id = parseInt(route.params.valueRecordId as string)
|
||||||
|
try {
|
||||||
|
const api = new AccountApiClient(route)
|
||||||
|
valueRecord.value = await api.getValueRecord(accountId, id)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
await router.replace('/')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function deleteValueRecord() {
|
||||||
|
if (!valueRecord.value) return
|
||||||
|
const confirm = await showConfirm("Are you sure you want to delete this value record? This may affect how your account's balance is calculated.")
|
||||||
|
if (!confirm) return
|
||||||
|
const api = new AccountApiClient(route)
|
||||||
|
try {
|
||||||
|
await api.deleteValueRecord(valueRecord.value.accountId, valueRecord.value.id)
|
||||||
|
await router.replace(`/profiles/${getSelectedProfile(route)}/accounts/${valueRecord.value.accountId}`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<AppPage title="Value Record" v-if="valueRecord">
|
||||||
|
<PropertiesTable>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<td>{{ valueRecord.id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Value</th>
|
||||||
|
<td>
|
||||||
|
<AppBadge>{{ formatMoney(valueRecord.value, valueRecord.currency) }}</AppBadge>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</PropertiesTable>
|
||||||
|
<div v-if="valueRecord.attachments.length > 0">
|
||||||
|
<h3>Attachments</h3>
|
||||||
|
<AttachmentRow v-for="a in valueRecord.attachments" :attachment="a" :key="a.id" disabled />
|
||||||
|
</div>
|
||||||
|
<ButtonBar>
|
||||||
|
<AppButton type="button" icon="trash" size="sm" @click="deleteValueRecord()">
|
||||||
|
Delete this record
|
||||||
|
</AppButton>
|
||||||
|
</ButtonBar>
|
||||||
|
|
||||||
|
</AppPage>
|
||||||
|
</template>
|
||||||
|
|
@ -3,6 +3,7 @@ import { AccountApiClient, type Account, type CurrencyBalance } from '@/api/acco
|
||||||
import { formatMoney } from '@/api/data'
|
import { formatMoney } from '@/api/data'
|
||||||
import { getSelectedProfile } from '@/api/profile'
|
import { getSelectedProfile } from '@/api/profile'
|
||||||
import AccountCard from '@/components/AccountCard.vue'
|
import AccountCard from '@/components/AccountCard.vue'
|
||||||
|
import AppBadge from '@/components/common/AppBadge.vue'
|
||||||
import AppButton from '@/components/common/AppButton.vue'
|
import AppButton from '@/components/common/AppButton.vue'
|
||||||
import HomeModule from '@/components/HomeModule.vue'
|
import HomeModule from '@/components/HomeModule.vue'
|
||||||
import { onMounted, ref, type Ref } from 'vue'
|
import { onMounted, ref, type Ref } from 'vue'
|
||||||
|
|
@ -33,13 +34,9 @@ onMounted(async () => {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<AppBadge v-for="bal in totalBalances" :key="bal.currency.code">
|
||||||
<li v-for="bal in totalBalances" :key="bal.currency.code">
|
Total {{ bal.currency.code }}: {{ formatMoney(bal.balance, bal.currency) }}
|
||||||
<span>Total in {{ bal.currency.code }}</span>
|
</AppBadge>
|
||||||
=
|
|
||||||
<span>{{ formatMoney(bal.balance, bal.currency) }}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:actions>
|
<template v-slot:actions>
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,11 @@ const router = createRouter({
|
||||||
component: () => import('@/pages/forms/EditAccountPage.vue'),
|
component: () => import('@/pages/forms/EditAccountPage.vue'),
|
||||||
meta: { title: 'Edit Account' },
|
meta: { title: 'Edit Account' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'accounts/:accountId/value-records/:valueRecordId',
|
||||||
|
component: () => import('@/pages/ValueRecordPage.vue'),
|
||||||
|
meta: { title: 'Value Record' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'add-account',
|
path: 'add-account',
|
||||||
component: () => import('@/pages/forms/EditAccountPage.vue'),
|
component: () => import('@/pages/forms/EditAccountPage.vue'),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue