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 { AccountApiClient, AccountValueRecordType, type Account, type AccountValueRecord, type AccountValueRecordCreationPayload } from '@/api/account';
|
||||
import { useProfileStore } from '@/stores/profile-store';
|
||||
import { datetimeLocalToISO, getDatetimeLocalValueForNow } from '@/util/time';
|
||||
|
||||
const props = defineProps<{ account: Account }>()
|
||||
const profileStore = useProfileStore()
|
||||
|
|
@ -19,10 +20,7 @@ const amount = ref(0)
|
|||
|
||||
async function show(): Promise<AccountValueRecord | undefined> {
|
||||
if (!modal.value) return Promise.resolve(undefined)
|
||||
const now = new Date()
|
||||
const localDate = new Date(now.getTime() - now.getTimezoneOffset() * 60_000)
|
||||
localDate.setMilliseconds(0)
|
||||
timestamp.value = localDate.toISOString().slice(0, -1)
|
||||
timestamp.value = getDatetimeLocalValueForNow()
|
||||
amount.value = props.account.currentBalance ?? 0
|
||||
savedValueRecord.value = undefined
|
||||
const result = await modal.value.show()
|
||||
|
|
@ -35,7 +33,7 @@ async function show(): Promise<AccountValueRecord | undefined> {
|
|||
async function addValueRecord() {
|
||||
if (!profileStore.state) return
|
||||
const payload: AccountValueRecordCreationPayload = {
|
||||
timestamp: new Date(timestamp.value).toISOString(),
|
||||
timestamp: datetimeLocalToISO(timestamp.value),
|
||||
type: AccountValueRecordType.BALANCE,
|
||||
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 AppButton from '@/components/AppButton.vue';
|
||||
import AppPage from '@/components/AppPage.vue';
|
||||
import CategoryLabel from '@/components/CategoryLabel.vue';
|
||||
import PropertiesTable from '@/components/PropertiesTable.vue';
|
||||
import TagLabel from '@/components/TagLabel.vue';
|
||||
import { useProfileStore } from '@/stores/profile-store';
|
||||
import { showAlert, showConfirm } from '@/util/alert';
|
||||
import { onMounted, ref, type Ref } from 'vue';
|
||||
|
|
@ -70,7 +72,7 @@ async function deleteTransaction() {
|
|||
<tr v-if="transaction.category">
|
||||
<th>Category</th>
|
||||
<td>
|
||||
{{ transaction.category.name }}
|
||||
<CategoryLabel :category="transaction.category" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="transaction.creditedAccount">
|
||||
|
|
@ -88,7 +90,7 @@ async function deleteTransaction() {
|
|||
<tr>
|
||||
<th>Tags</th>
|
||||
<td>
|
||||
<span v-for="tag in transaction.tags" :key="tag">{{ tag }},</span>
|
||||
<TagLabel v-for="t in transaction.tags" :key="t" :tag="t" />
|
||||
</td>
|
||||
</tr>
|
||||
</PropertiesTable>
|
||||
|
|
|
|||
|
|
@ -54,12 +54,11 @@ async function checkAuth() {
|
|||
<h1 class="app-header-text" @click="router.push('/')">Finnow</h1>
|
||||
</div>
|
||||
<div>
|
||||
<span class="app-user-widget">
|
||||
Welcome, <RouterLink to="/me" style="color: var(--bg-primary)">{{ authStore.state?.username }}</RouterLink>
|
||||
<span class="app-user-widget" @click="router.push('/me')">
|
||||
<font-awesome-icon icon="fa-user"></font-awesome-icon>
|
||||
</span>
|
||||
|
||||
<span class="app-logout-button" @click="authStore.onUserLoggedOut()">
|
||||
Log out
|
||||
<font-awesome-icon icon="fa-solid fa-arrow-right-from-bracket"></font-awesome-icon>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -93,18 +92,26 @@ async function checkAuth() {
|
|||
|
||||
.app-logout-button {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
margin-right: 1em;
|
||||
margin-right: 0.5em;
|
||||
background-color: #0099d1;
|
||||
border-radius: 50%;
|
||||
padding: 0.35rem;
|
||||
}
|
||||
|
||||
.app-logout-button:hover {
|
||||
color: var(--bg-secondary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.app-user-widget {
|
||||
margin-right: 1em;
|
||||
font-weight: bold;
|
||||
margin-right: 0.5em;
|
||||
cursor: pointer;
|
||||
background-color: #0099d1;
|
||||
border-radius: 50%;
|
||||
padding: 0.35rem;
|
||||
}
|
||||
|
||||
.app-user-widget:hover {
|
||||
color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.app-header-link {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ import FormActions from '@/components/form/FormActions.vue';
|
|||
import FormControl from '@/components/form/FormControl.vue';
|
||||
import FormGroup from '@/components/form/FormGroup.vue';
|
||||
import LineItemsEditor from '@/components/LineItemsEditor.vue';
|
||||
import TagLabel from '@/components/TagLabel.vue';
|
||||
import { useProfileStore } from '@/stores/profile-store';
|
||||
import { getDatetimeLocalValueForNow } from '@/util/time';
|
||||
import { computed, onMounted, ref, watch, type Ref } from 'vue';
|
||||
import { useRoute, useRouter, } from 'vue-router';
|
||||
|
||||
|
|
@ -99,6 +101,10 @@ onMounted(async () => {
|
|||
} finally {
|
||||
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.
|
||||
*/
|
||||
function isEdited() {
|
||||
if (!existingTransaction.value) return false
|
||||
if (!existingTransaction.value) return true
|
||||
const tagsEqual = tags.value.every(t => existingTransaction.value?.tags.includes(t)) &&
|
||||
existingTransaction.value.tags.every(t => tags.value.includes(t))
|
||||
let lineItemsEqual = false
|
||||
|
|
@ -308,12 +314,7 @@ function isEdited() {
|
|||
<!-- Tags -->
|
||||
<FormControl label="Tags">
|
||||
<div style="margin-top: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<span v-for="tag in tags" :key="tag"
|
||||
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>
|
||||
<TagLabel v-for="t in tags" :key="t" :tag="t" deletable @deleted="tags = tags.filter(tg => tg !== t)" />
|
||||
</div>
|
||||
<div>
|
||||
<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