Added labels for category and tags, utils for default times.
Build and Deploy Web App / build-and-deploy (push) Successful in 17s
Details
Build and Deploy Web App / build-and-deploy (push) Successful in 17s
Details
This commit is contained in:
parent
05820e6e0e
commit
7a74d5f17e
|
|
@ -7,6 +7,7 @@ import ModalWrapper from './ModalWrapper.vue';
|
||||||
import AppButton from './AppButton.vue';
|
import AppButton from './AppButton.vue';
|
||||||
import { AccountApiClient, AccountValueRecordType, type Account, type AccountValueRecord, type AccountValueRecordCreationPayload } from '@/api/account';
|
import { AccountApiClient, AccountValueRecordType, type Account, type AccountValueRecord, type AccountValueRecordCreationPayload } from '@/api/account';
|
||||||
import { useProfileStore } from '@/stores/profile-store';
|
import { useProfileStore } from '@/stores/profile-store';
|
||||||
|
import { datetimeLocalToISO, getDatetimeLocalValueForNow } from '@/util/time';
|
||||||
|
|
||||||
const props = defineProps<{ account: Account }>()
|
const props = defineProps<{ account: Account }>()
|
||||||
const profileStore = useProfileStore()
|
const profileStore = useProfileStore()
|
||||||
|
|
@ -19,10 +20,7 @@ const amount = ref(0)
|
||||||
|
|
||||||
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)
|
||||||
const now = new Date()
|
timestamp.value = getDatetimeLocalValueForNow()
|
||||||
const localDate = new Date(now.getTime() - now.getTimezoneOffset() * 60_000)
|
|
||||||
localDate.setMilliseconds(0)
|
|
||||||
timestamp.value = localDate.toISOString().slice(0, -1)
|
|
||||||
amount.value = props.account.currentBalance ?? 0
|
amount.value = props.account.currentBalance ?? 0
|
||||||
savedValueRecord.value = undefined
|
savedValueRecord.value = undefined
|
||||||
const result = await modal.value.show()
|
const result = await modal.value.show()
|
||||||
|
|
@ -35,7 +33,7 @@ async function show(): Promise<AccountValueRecord | undefined> {
|
||||||
async function addValueRecord() {
|
async function addValueRecord() {
|
||||||
if (!profileStore.state) return
|
if (!profileStore.state) return
|
||||||
const payload: AccountValueRecordCreationPayload = {
|
const payload: AccountValueRecordCreationPayload = {
|
||||||
timestamp: new Date(timestamp.value).toISOString(),
|
timestamp: datetimeLocalToISO(timestamp.value),
|
||||||
type: AccountValueRecordType.BALANCE,
|
type: AccountValueRecordType.BALANCE,
|
||||||
value: amount.value
|
value: amount.value
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TransactionCategory } from '@/api/transaction';
|
||||||
|
|
||||||
|
defineProps<{ category: TransactionCategory }>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<span class="category-label">
|
||||||
|
<div class="category-label-color" :style="{ 'background-color': '#' + category.color }"></div>
|
||||||
|
{{ category.name }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<style lang="css">
|
||||||
|
.category-label {
|
||||||
|
margin: 0.25rem;
|
||||||
|
padding: 0.1rem 0.25rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-label-color {
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 0.1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{ tag: string, deletable?: boolean }>()
|
||||||
|
defineEmits<{ deleted: void }>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<span class="tag-label">
|
||||||
|
<span class="tag-label-hashtag">#</span>
|
||||||
|
{{ tag }}
|
||||||
|
<font-awesome-icon v-if="deletable" icon="fa-x" class="tag-label-delete"
|
||||||
|
@click="$emit('deleted')"></font-awesome-icon>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<style lang="css">
|
||||||
|
.tag-label {
|
||||||
|
margin: 0.25rem;
|
||||||
|
padding: 0.1rem 0.25rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-label-hashtag {
|
||||||
|
color: rgb(82, 82, 102);
|
||||||
|
margin-right: -0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-label-delete {
|
||||||
|
color: gray;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -4,7 +4,9 @@ import { formatMoney } from '@/api/data';
|
||||||
import { TransactionApiClient, type TransactionDetail } from '@/api/transaction';
|
import { TransactionApiClient, type TransactionDetail } from '@/api/transaction';
|
||||||
import AppButton from '@/components/AppButton.vue';
|
import AppButton from '@/components/AppButton.vue';
|
||||||
import AppPage from '@/components/AppPage.vue';
|
import AppPage from '@/components/AppPage.vue';
|
||||||
|
import CategoryLabel from '@/components/CategoryLabel.vue';
|
||||||
import PropertiesTable from '@/components/PropertiesTable.vue';
|
import PropertiesTable from '@/components/PropertiesTable.vue';
|
||||||
|
import TagLabel from '@/components/TagLabel.vue';
|
||||||
import { useProfileStore } from '@/stores/profile-store';
|
import { useProfileStore } from '@/stores/profile-store';
|
||||||
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';
|
||||||
|
|
@ -70,7 +72,7 @@ async function deleteTransaction() {
|
||||||
<tr v-if="transaction.category">
|
<tr v-if="transaction.category">
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<td>
|
<td>
|
||||||
{{ transaction.category.name }}
|
<CategoryLabel :category="transaction.category" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="transaction.creditedAccount">
|
<tr v-if="transaction.creditedAccount">
|
||||||
|
|
@ -88,7 +90,7 @@ async function deleteTransaction() {
|
||||||
<tr>
|
<tr>
|
||||||
<th>Tags</th>
|
<th>Tags</th>
|
||||||
<td>
|
<td>
|
||||||
<span v-for="tag in transaction.tags" :key="tag">{{ tag }},</span>
|
<TagLabel v-for="t in transaction.tags" :key="t" :tag="t" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</PropertiesTable>
|
</PropertiesTable>
|
||||||
|
|
|
||||||
|
|
@ -54,12 +54,11 @@ async function checkAuth() {
|
||||||
<h1 class="app-header-text" @click="router.push('/')">Finnow</h1>
|
<h1 class="app-header-text" @click="router.push('/')">Finnow</h1>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="app-user-widget">
|
<span class="app-user-widget" @click="router.push('/me')">
|
||||||
Welcome, <RouterLink to="/me" style="color: var(--bg-primary)">{{ authStore.state?.username }}</RouterLink>
|
<font-awesome-icon icon="fa-user"></font-awesome-icon>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="app-logout-button" @click="authStore.onUserLoggedOut()">
|
<span class="app-logout-button" @click="authStore.onUserLoggedOut()">
|
||||||
Log out
|
|
||||||
<font-awesome-icon icon="fa-solid fa-arrow-right-from-bracket"></font-awesome-icon>
|
<font-awesome-icon icon="fa-solid fa-arrow-right-from-bracket"></font-awesome-icon>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -93,18 +92,26 @@ async function checkAuth() {
|
||||||
|
|
||||||
.app-logout-button {
|
.app-logout-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: bold;
|
margin-right: 0.5em;
|
||||||
margin-right: 1em;
|
background-color: #0099d1;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-logout-button:hover {
|
.app-logout-button:hover {
|
||||||
color: var(--bg-secondary);
|
color: var(--bg-secondary);
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-user-widget {
|
.app-user-widget {
|
||||||
margin-right: 1em;
|
margin-right: 0.5em;
|
||||||
font-weight: bold;
|
cursor: pointer;
|
||||||
|
background-color: #0099d1;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-user-widget:hover {
|
||||||
|
color: var(--bg-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header-link {
|
.app-header-link {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ import FormActions from '@/components/form/FormActions.vue';
|
||||||
import FormControl from '@/components/form/FormControl.vue';
|
import FormControl from '@/components/form/FormControl.vue';
|
||||||
import FormGroup from '@/components/form/FormGroup.vue';
|
import FormGroup from '@/components/form/FormGroup.vue';
|
||||||
import LineItemsEditor from '@/components/LineItemsEditor.vue';
|
import LineItemsEditor from '@/components/LineItemsEditor.vue';
|
||||||
|
import TagLabel from '@/components/TagLabel.vue';
|
||||||
import { useProfileStore } from '@/stores/profile-store';
|
import { useProfileStore } from '@/stores/profile-store';
|
||||||
|
import { getDatetimeLocalValueForNow } from '@/util/time';
|
||||||
import { computed, onMounted, ref, watch, type Ref } from 'vue';
|
import { computed, onMounted, ref, watch, type Ref } from 'vue';
|
||||||
import { useRoute, useRouter, } from 'vue-router';
|
import { useRoute, useRouter, } from 'vue-router';
|
||||||
|
|
||||||
|
|
@ -99,6 +101,10 @@ onMounted(async () => {
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Load default values.
|
||||||
|
timestamp.value = getDatetimeLocalValueForNow()
|
||||||
|
amount.value = Math.pow(10, currency.value?.fractionalDigits ?? 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -210,7 +216,7 @@ function isFormValid() {
|
||||||
* least one edit to it. Otherwise, there's no point in saving.
|
* least one edit to it. Otherwise, there's no point in saving.
|
||||||
*/
|
*/
|
||||||
function isEdited() {
|
function isEdited() {
|
||||||
if (!existingTransaction.value) return false
|
if (!existingTransaction.value) return true
|
||||||
const tagsEqual = tags.value.every(t => existingTransaction.value?.tags.includes(t)) &&
|
const tagsEqual = tags.value.every(t => existingTransaction.value?.tags.includes(t)) &&
|
||||||
existingTransaction.value.tags.every(t => tags.value.includes(t))
|
existingTransaction.value.tags.every(t => tags.value.includes(t))
|
||||||
let lineItemsEqual = false
|
let lineItemsEqual = false
|
||||||
|
|
@ -308,12 +314,7 @@ function isEdited() {
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<FormControl label="Tags">
|
<FormControl label="Tags">
|
||||||
<div style="margin-top: 0.5rem; margin-bottom: 0.5rem;">
|
<div style="margin-top: 0.5rem; margin-bottom: 0.5rem;">
|
||||||
<span v-for="tag in tags" :key="tag"
|
<TagLabel v-for="t in tags" :key="t" :tag="t" deletable @deleted="tags = tags.filter(tg => tg !== t)" />
|
||||||
style="margin: 0.25rem; padding: 0.25rem 0.75rem; background-color: var(--bg-secondary); border-radius: 0.5rem;">
|
|
||||||
{{ tag }}
|
|
||||||
<font-awesome-icon :icon="'fa-x'" style="color: gray; cursor: pointer;"
|
|
||||||
@click="tags = tags.filter(t => t !== tag)"></font-awesome-icon>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<select v-model="selectedTagToAdd">
|
<select v-model="selectedTagToAdd">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
export function getDatetimeLocalValueForNow(): string {
|
||||||
|
const now = new Date()
|
||||||
|
const localDate = new Date(now.getTime() - now.getTimezoneOffset() * 60_000)
|
||||||
|
localDate.setMilliseconds(0)
|
||||||
|
return localDate.toISOString().slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function datetimeLocalToISO(s: string): string {
|
||||||
|
return new Date(s).toISOString()
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue