Added vendor page.
Build and Deploy Web App / build-and-deploy (push) Successful in 34s
Details
Build and Deploy Web App / build-and-deploy (push) Successful in 34s
Details
This commit is contained in:
parent
0e3b615684
commit
6415311925
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ abstract class FileListItem {
|
|||
public readonly filename: string,
|
||||
public readonly contentType: string,
|
||||
public readonly size: number,
|
||||
) { }
|
||||
) {}
|
||||
}
|
||||
|
||||
class ExistingFileListItem extends FileListItem {
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue