Added transaction card.
Build and Deploy Web App / build-and-deploy (push) Successful in 20s
Details
Build and Deploy Web App / build-and-deploy (push) Successful in 20s
Details
This commit is contained in:
parent
c2fef7edde
commit
da01198e0c
|
|
@ -49,6 +49,12 @@ a:hover {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.app-module-container {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* A generic table styling for most default tables. */
|
/* A generic table styling for most default tables. */
|
||||||
.app-table {
|
.app-table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ function goToAccount() {
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
.account-card {
|
.account-card {
|
||||||
background-color: var(--bg-primary);
|
background-color: var(--bg-primary);
|
||||||
padding: 0.5rem 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getSelectedProfile } from '@/api/profile';
|
import { getSelectedProfile } from '@/api/profile';
|
||||||
import type { TransactionCategory } from '@/api/transaction';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
interface CategoryInfo {
|
||||||
|
name: string
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const props = defineProps<{ category: TransactionCategory, clickable?: boolean }>()
|
const props = defineProps<{ category: CategoryInfo, clickable?: boolean }>()
|
||||||
|
|
||||||
function onClicked() {
|
function onClicked() {
|
||||||
if (props.clickable) {
|
if (props.clickable) {
|
||||||
|
|
@ -14,7 +19,7 @@ function onClicked() {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<span class="category-label" @click="onClicked()" :style="{ 'cursor': clickable ? 'pointer' : 'default' }">
|
<span class="category-label" @click="onClicked()" :style="{ 'cursor': clickable ? 'pointer' : 'inherit' }">
|
||||||
<div class="category-label-color" :style="{ 'background-color': '#' + category.color }"></div>
|
<div class="category-label-color" :style="{ 'background-color': '#' + category.color }"></div>
|
||||||
{{ category.name }}
|
{{ category.name }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { formatMoney } from '@/api/data';
|
||||||
|
import { getSelectedProfile } from '@/api/profile';
|
||||||
|
import type { TransactionsListItem } from '@/api/transaction';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import CategoryLabel from './CategoryLabel.vue';
|
||||||
|
import { computed, type Ref } from 'vue';
|
||||||
|
import { AccountTypes } from '@/api/account';
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
type MoneyStyle = "positive" | "negative" | "neutral"
|
||||||
|
|
||||||
|
const props = defineProps<{ tx: TransactionsListItem }>()
|
||||||
|
// Defines the style to use for money based on which accounts are involved.
|
||||||
|
const moneyStyle: Ref<MoneyStyle> = computed(() => {
|
||||||
|
if (props.tx.debitedAccount !== null && props.tx.creditedAccount === null) {
|
||||||
|
const debitedAccountType = AccountTypes.of(props.tx.debitedAccount.type)
|
||||||
|
return debitedAccountType.debitsPositive
|
||||||
|
? "positive"
|
||||||
|
: "negative"
|
||||||
|
} else if (props.tx.creditedAccount !== null && props.tx.debitedAccount === null) {
|
||||||
|
const creditedAccountType = AccountTypes.of(props.tx.creditedAccount.type)
|
||||||
|
return creditedAccountType.debitsPositive
|
||||||
|
? "negative"
|
||||||
|
: "positive"
|
||||||
|
}
|
||||||
|
return "neutral"
|
||||||
|
})
|
||||||
|
|
||||||
|
function goToTransaction() {
|
||||||
|
const profile = getSelectedProfile(route)
|
||||||
|
router.push(`/profiles/${profile}/transactions/${props.tx.id}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="transaction-card" @click="goToTransaction()">
|
||||||
|
<!-- Top row contains timestamp and amount. -->
|
||||||
|
<div class="transaction-card-top-row">
|
||||||
|
<div>
|
||||||
|
<div class="transaction-card-id">Transaction #{{ tx.id }}</div>
|
||||||
|
<div class="transaction-card-timestamp">
|
||||||
|
{{ new Date(tx.timestamp).toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="transaction-card-money" :class="{
|
||||||
|
'transaction-card-money-positive': moneyStyle === 'positive',
|
||||||
|
'transaction-card-money-negative': moneyStyle === 'negative'
|
||||||
|
}">
|
||||||
|
{{ formatMoney(tx.amount, tx.currency) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="tx.creditedAccount !== null" class="transaction-card-account-label">
|
||||||
|
Credited to <span class="transaction-card-account-badge">{{ tx.creditedAccount.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tx.debitedAccount !== null" class="transaction-card-account-label">
|
||||||
|
Debited to <span class="transaction-card-account-badge">{{ tx.debitedAccount.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle row contains the description. -->
|
||||||
|
<div>
|
||||||
|
<p class="transaction-card-description">{{ tx.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom row contains other links. -->
|
||||||
|
<div>
|
||||||
|
<CategoryLabel :category="tx.category" v-if="tx.category" style="margin-left: 0" />
|
||||||
|
<span class="transaction-card-vendor-label" v-if="tx.vendor">{{ tx.vendor.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="css">
|
||||||
|
.transaction-card {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card:hover {
|
||||||
|
background-color: var(--bg-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card-top-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card-id {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card-timestamp {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-family: monospace;
|
||||||
|
font-weight: 400;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card-money {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: monospace;
|
||||||
|
color: white;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card-money-positive {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card-money-negative {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card-account-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: gray;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card-account-badge {
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card-description {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card-vendor-label {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
padding: 0.1rem 0.2rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { formatMoney } from '@/api/data';
|
|
||||||
import type { Page, PageRequest } from '@/api/pagination';
|
import type { Page, PageRequest } from '@/api/pagination';
|
||||||
import { getSelectedProfile } from '@/api/profile';
|
import { getSelectedProfile } from '@/api/profile';
|
||||||
import { TransactionApiClient, type TransactionsListItem } from '@/api/transaction';
|
import { TransactionApiClient, type TransactionsListItem } from '@/api/transaction';
|
||||||
|
|
@ -8,10 +7,11 @@ import HomeModule from '@/components/HomeModule.vue';
|
||||||
import PaginationControls from '@/components/common/PaginationControls.vue';
|
import PaginationControls from '@/components/common/PaginationControls.vue';
|
||||||
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 TransactionCard from '@/components/TransactionCard.vue';
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const transactions: Ref<Page<TransactionsListItem>> = ref({ items: [], pageRequest: { page: 1, size: 10, sorts: [] }, totalElements: 0, totalPages: 0, isFirst: true, isLast: true })
|
const transactions: Ref<Page<TransactionsListItem>> = ref({ items: [], pageRequest: { page: 1, size: 5, sorts: [] }, totalElements: 0, totalPages: 0, isFirst: true, isLast: true })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchPage(transactions.value.pageRequest)
|
await fetchPage(transactions.value.pageRequest)
|
||||||
|
|
@ -29,47 +29,9 @@ async function fetchPage(pageRequest: PageRequest) {
|
||||||
<template>
|
<template>
|
||||||
<HomeModule title="Transactions">
|
<HomeModule title="Transactions">
|
||||||
<template v-slot:default>
|
<template v-slot:default>
|
||||||
<div v-if="transactions.totalElements > 0">
|
<TransactionCard v-for="tx in transactions.items" :key="tx.id" :tx="tx" />
|
||||||
<table class="app-table">
|
|
||||||
<thead>
|
<PaginationControls :page="transactions" @update="pr => fetchPage(pr)"></PaginationControls>
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
<th>Currency</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Credited Account</th>
|
|
||||||
<th>Debited Account</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="tx in transactions.items" :key="tx.id">
|
|
||||||
<td>
|
|
||||||
{{ new Date(tx.timestamp).toLocaleDateString() }}
|
|
||||||
</td>
|
|
||||||
<td style="text-align: right;">{{ formatMoney(tx.amount, tx.currency) }}</td>
|
|
||||||
<td>{{ tx.currency.code }}</td>
|
|
||||||
<td>{{ tx.description }}</td>
|
|
||||||
<td>
|
|
||||||
<RouterLink v-if="tx.creditedAccount"
|
|
||||||
:to="`/profiles/${getSelectedProfile(route)}/accounts/${tx.creditedAccount.id}`">
|
|
||||||
{{ tx.creditedAccount?.name }}
|
|
||||||
</RouterLink>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<RouterLink v-if="tx.debitedAccount"
|
|
||||||
:to="`/profiles/${getSelectedProfile(route)}/accounts/${tx.debitedAccount.id}`">
|
|
||||||
{{ tx.debitedAccount?.name }}
|
|
||||||
</RouterLink>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<RouterLink :to="`/profiles/${getSelectedProfile(route)}/transactions/${tx.id}`">View</RouterLink>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<PaginationControls :page="transactions" @update="pr => fetchPage(pr)"></PaginationControls>
|
|
||||||
</div>
|
|
||||||
<p v-if="transactions.totalElements === 0">
|
<p v-if="transactions.totalElements === 0">
|
||||||
You haven't added any transactions.
|
You haven't added any transactions.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue