Added labels for category and tags, utils for default times.
Build and Deploy Web App / build-and-deploy (push) Successful in 17s Details

This commit is contained in:
andrewlalis 2025-08-22 21:47:20 -04:00
parent 05820e6e0e
commit 7a74d5f17e
7 changed files with 100 additions and 22 deletions

View File

@ -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
} }

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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">

10
web-app/src/util/time.ts Normal file
View File

@ -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()
}