Added vendor page.
Build and Deploy Web App / build-and-deploy (push) Successful in 34s Details

This commit is contained in:
andrewlalis 2025-12-01 19:44:04 -05:00
parent 0e3b615684
commit 6415311925
9 changed files with 202 additions and 42 deletions

View File

@ -10,6 +10,14 @@
margin-top: 0.5rem;
}
.mt-0 {
margin-top: 0;
}
.mb-0 {
margin-bottom: 0;
}
.mx-1 {
margin-left: 0.5rem;
margin-right: 0.5rem;

View File

@ -1,15 +1,25 @@
<script setup lang="ts">
import type { TransactionVendor } from '@/api/transaction'
import AppButton from './common/AppButton.vue'
import { useRoute } from 'vue-router'
import { getSelectedProfile } from '@/api/profile'
import { computed } from 'vue'
defineProps<{ vendor: TransactionVendor }>()
const props = defineProps<{ vendor: TransactionVendor }>()
defineEmits<{ edit: []; delete: [] }>()
const route = useRoute()
const routeToVendor = computed(() => {
const profile = getSelectedProfile(route)
return `/profiles/${profile}/vendors/${props.vendor.id}`
})
</script>
<template>
<div class="vendor-card">
<div style="flex-shrink: 1">
<div>
{{ vendor.name }}
<RouterLink :to="routeToVendor">{{ vendor.name }}</RouterLink>
</div>
<div
v-if="vendor.description !== null"

View File

@ -71,9 +71,7 @@ onMounted(() => {
function syncModelsWithInternalFileList() {
// Compute the set of uploaded files as just any newly uploaded file list item.
uploadedFiles.value = files.value
.filter((f) => f instanceof NewFileListItem)
.map((f) => f.file)
uploadedFiles.value = files.value.filter((f) => f instanceof NewFileListItem).map((f) => f.file)
// Compute the set of removed files as those from the set of initial files whose ID is no longer present.
const retainedExistingFileIds = files.value
.filter((f) => f instanceof ExistingFileListItem)
@ -116,14 +114,33 @@ function onFileDeleteClicked(idx: number) {
<template>
<div class="file-selector">
<div @click.prevent="">
<AttachmentRow v-for="(file, idx) in files" :key="idx" :attachment="file" @deleted="onFileDeleteClicked(idx)" />
<AttachmentRow
v-for="(file, idx) in files"
:key="idx"
:attachment="file"
@deleted="onFileDeleteClicked(idx)"
/>
</div>
<div>
<input id="fileInput" type="file" capture="environment" accept="image/*,text/*,.pdf,.doc,.odt,.docx,.xlsx"
multiple @change="onFileInputChanged" style="display: none" ref="fileInput" :disabled="disabled" />
<input
id="fileInput"
type="file"
capture="environment"
accept="image/*,text/*,.pdf,.doc,.odt,.docx,.xlsx"
multiple
@change="onFileInputChanged"
style="display: none"
ref="fileInput"
:disabled="disabled"
/>
<label for="fileInput">
<AppButton icon="upload" type="button" @click="fileInput?.click()" :disabled="disabled">Select a File
<AppButton
icon="upload"
type="button"
@click="fileInput?.click()"
:disabled="disabled"
>Select a File
</AppButton>
</label>
</div>

View File

@ -85,6 +85,14 @@ async function deleteTransaction() {
console.error(err)
}
}
async function onVendorClicked() {
if (transaction.value && transaction.value.vendor) {
await router.push(
`/profiles/${getSelectedProfile(route)}/vendors/${transaction.value.vendor.id}`,
)
}
}
</script>
<template>
<AppPage
@ -102,6 +110,8 @@ async function deleteTransaction() {
<AppBadge
size="md"
v-if="transaction.vendor"
style="cursor: pointer"
@click="onVendorClicked()"
>
{{ transaction.vendor.name }}
</AppBadge>

View File

@ -2,7 +2,7 @@
import ProfileModule from './home/ProfileModule.vue'
import AccountsModule from './home/AccountsModule.vue'
import TransactionsModule from './home/TransactionsModule.vue'
import AnalyticsModule from './home/AnalyticsModule.vue';
import AnalyticsModule from './home/AnalyticsModule.vue'
</script>
<template>
<div class="app-module-container">

View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import { defaultPage, type Page, type PageRequest } from '@/api/pagination'
import { getSelectedProfile } from '@/api/profile'
import {
TransactionApiClient,
type TransactionsListItem,
type TransactionVendor,
} from '@/api/transaction'
import AppPage from '@/components/common/AppPage.vue'
import PaginationControls from '@/components/common/PaginationControls.vue'
import TransactionCard from '@/components/TransactionCard.vue'
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const vendor = ref<TransactionVendor | undefined>()
const relatedTransactionsPage = ref<Page<TransactionsListItem>>(defaultPage())
onMounted(async () => {
const vendorId = parseInt(route.params.id as string)
try {
const api = new TransactionApiClient(getSelectedProfile(route))
vendor.value = await api.getVendor(vendorId)
await fetchPage(1)
} catch (err) {
console.error(err)
await router.replace('/')
}
})
async function fetchPage(pg: number) {
if (!vendor.value) return
const pageRequest: PageRequest = {
page: pg,
size: 10,
sorts: [
{
attribute: 'txn.timestamp',
dir: 'DESC',
},
],
}
const params = new URLSearchParams()
params.append('vendor', vendor.value?.id + '')
const api = new TransactionApiClient(getSelectedProfile(route))
relatedTransactionsPage.value = await api.searchTransactions(params, pageRequest)
}
</script>
<template>
<AppPage
v-if="vendor"
:title="'Vendor - ' + vendor.name"
>
<p>
{{ vendor.description }}
</p>
<div>
<h3 class="mb-0">Transactions</h3>
<p class="text-muted font-size-small mt-0">
Below is a list of all transactions recorded with this vendor.
</p>
<PaginationControls
:page="relatedTransactionsPage"
@update="(pr) => fetchPage(pr.page)"
class="align-right"
/>
<TransactionCard
v-for="txn in relatedTransactionsPage.items"
:key="txn.id"
:tx="txn"
/>
</div>
</AppPage>
</template>

View File

@ -7,7 +7,7 @@ import ButtonBar from '@/components/common/ButtonBar.vue'
import EditVendorModal from '@/components/EditVendorModal.vue'
import VendorCard from '@/components/VendorCard.vue'
import { showConfirm } from '@/util/alert'
import { onMounted, ref, useTemplateRef, type Ref } from 'vue'
import { nextTick, onMounted, ref, useTemplateRef, type Ref } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
@ -27,6 +27,7 @@ async function loadVendors() {
async function addVendor() {
editedVendor.value = undefined
await nextTick()
const result = await editVendorModal.value?.show()
if (result === 'saved') {
await loadVendors()
@ -35,6 +36,7 @@ async function addVendor() {
async function editVendor(vendor: TransactionVendor) {
editedVendor.value = vendor
await nextTick()
const result = await editVendorModal.value?.show()
if (result === 'saved') {
await loadVendors()

View File

@ -1,21 +1,45 @@
<script setup lang="ts">
import { AccountApiClient } from '@/api/account';
import HomeModule from '@/components/HomeModule.vue';
import { CategoryScale, Chart, Filler, Legend, LinearScale, LineElement, PointElement, TimeScale, Title, Tooltip, type ChartData, type ChartDataset, type ChartOptions } from 'chart.js';
import { getTime } from 'date-fns';
import { onMounted, ref } from 'vue';
import { Line } from 'vue-chartjs';
import { useRoute } from 'vue-router';
import 'chartjs-adapter-date-fns';
import { integerMoneyToFloat } from '@/api/data';
import { AnalyticsApiClient } from '@/api/analytics';
import { AccountApiClient } from '@/api/account'
import HomeModule from '@/components/HomeModule.vue'
import {
CategoryScale,
Chart,
Filler,
Legend,
LinearScale,
LineElement,
PointElement,
TimeScale,
Title,
Tooltip,
type ChartData,
type ChartDataset,
type ChartOptions,
} from 'chart.js'
import { getTime } from 'date-fns'
import { onMounted, ref } from 'vue'
import { Line } from 'vue-chartjs'
import { useRoute } from 'vue-router'
import 'chartjs-adapter-date-fns'
import { integerMoneyToFloat } from '@/api/data'
import { AnalyticsApiClient } from '@/api/analytics'
const route = useRoute()
const chartData = ref<ChartData<"line"> | undefined>()
const chartOptions = ref<ChartOptions<"line"> | undefined>()
const chartData = ref<ChartData<'line'> | undefined>()
const chartOptions = ref<ChartOptions<'line'> | undefined>()
Chart.register(Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale, PointElement, TimeScale, Filler)
Chart.register(
Title,
Tooltip,
Legend,
LineElement,
CategoryScale,
LinearScale,
PointElement,
TimeScale,
Filler,
)
const COLORS = [
[255, 69, 69],
@ -27,7 +51,7 @@ const COLORS = [
[69, 125, 255],
[139, 69, 255],
[251, 69, 255],
[255, 69, 167]
[255, 69, 167],
]
onMounted(async () => {
@ -35,29 +59,29 @@ onMounted(async () => {
const analyticsApi = new AnalyticsApiClient(route)
const accounts = await api.getAccounts()
const timeSeriesData = await analyticsApi.getBalanceTimeSeries()
const datasets: ChartDataset<"line">[] = []
const datasets: ChartDataset<'line'>[] = []
// const timeZoneOffset = -(new Date().getTimezoneOffset())
let colorIdx = 0
for (const accountData of timeSeriesData.accounts) {
if (accountData.currencyCode !== 'USD') continue
const account = accounts.find(a => a.id === accountData.accountId)
const account = accounts.find((a) => a.id === accountData.accountId)
if (!account) {
console.warn("Couldn't find account id " + accountData.accountId)
continue
}
const color = COLORS[colorIdx++]
const points = accountData.balanceTimeSeries.map(p => {
const points = accountData.balanceTimeSeries.map((p) => {
return {
x: getTime(p.timestamp),
y: integerMoneyToFloat(p.balance, account.currency)
y: integerMoneyToFloat(p.balance, account.currency),
}
})
datasets.push({
label: "Account #" + account.numberSuffix,
label: 'Account #' + account.numberSuffix,
data: points,
cubicInterpolationMode: "monotone",
cubicInterpolationMode: 'monotone',
borderColor: `rgb(${color[0]}, ${color[1]}, ${color[2]})`,
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.25)`,
pointRadius: 0,
@ -68,28 +92,35 @@ onMounted(async () => {
}
chartData.value = {
datasets: datasets
datasets: datasets,
}
chartOptions.value = {
plugins: {
title: {
display: true,
text: "Account Balances"
}
text: 'Account Balances',
},
},
scales: {
x: {
type: "time",
type: 'time',
time: {
unit: "day"
}
}
}
unit: 'day',
},
},
},
}
})
</script>
<template>
<HomeModule title="Analytics" style="min-width: 500px;">
<Line v-if="chartData && chartOptions" :data="chartData" :options="chartOptions" />
<HomeModule
title="Analytics"
style="min-width: 500px"
>
<Line
v-if="chartData && chartOptions"
:data="chartData"
:options="chartOptions"
/>
</HomeModule>
</template>

View File

@ -82,6 +82,11 @@ const router = createRouter({
component: () => import('@/pages/VendorsPage.vue'),
meta: { title: 'Vendors' },
},
{
path: 'vendors/:id',
component: () => import('@/pages/VendorPage.vue'),
meta: { title: 'Vendor' },
},
{
path: 'categories',
component: () => import('@/pages/CategoriesPage.vue'),