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;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.app-module-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* A generic table styling for most default tables. */
|
||||
.app-table {
|
||||
border-collapse: collapse;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ function goToAccount() {
|
|||
<style lang="css">
|
||||
.account-card {
|
||||
background-color: var(--bg-primary);
|
||||
padding: 0.5rem 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import { getSelectedProfile } from '@/api/profile';
|
||||
import type { TransactionCategory } from '@/api/transaction';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
interface CategoryInfo {
|
||||
name: string
|
||||
color: string
|
||||
}
|
||||
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const props = defineProps<{ category: TransactionCategory, clickable?: boolean }>()
|
||||
const props = defineProps<{ category: CategoryInfo, clickable?: boolean }>()
|
||||
|
||||
function onClicked() {
|
||||
if (props.clickable) {
|
||||
|
|
@ -14,7 +19,7 @@ function onClicked() {
|
|||
}
|
||||
</script>
|
||||
<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>
|
||||
{{ category.name }}
|
||||
</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">
|
||||
import { formatMoney } from '@/api/data';
|
||||
import type { Page, PageRequest } from '@/api/pagination';
|
||||
import { getSelectedProfile } from '@/api/profile';
|
||||
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 { onMounted, ref, type Ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import TransactionCard from '@/components/TransactionCard.vue';
|
||||
|
||||
const router = useRouter()
|
||||
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 () => {
|
||||
await fetchPage(transactions.value.pageRequest)
|
||||
|
|
@ -29,47 +29,9 @@ async function fetchPage(pageRequest: PageRequest) {
|
|||
<template>
|
||||
<HomeModule title="Transactions">
|
||||
<template v-slot:default>
|
||||
<div v-if="transactions.totalElements > 0">
|
||||
<table class="app-table">
|
||||
<thead>
|
||||
<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>
|
||||
<TransactionCard v-for="tx in transactions.items" :key="tx.id" :tx="tx" />
|
||||
|
||||
<PaginationControls :page="transactions" @update="pr => fetchPage(pr)"></PaginationControls>
|
||||
</div>
|
||||
<p v-if="transactions.totalElements === 0">
|
||||
You haven't added any transactions.
|
||||
</p>
|
||||
|
|
|
|||
Loading…
Reference in New Issue