mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-19 09:14:27 +08:00
add chart tab to insights & explore page
This commit is contained in:
@@ -78,7 +78,7 @@
|
||||
<v-spacer/>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
|
||||
:disabled="loading" :icon="true"
|
||||
v-if="activeTab !== 'query'">
|
||||
v-if="activeTab === 'table' || activeTab === 'chart'">
|
||||
<v-icon :icon="mdiDotsVertical" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
@@ -102,7 +102,8 @@
|
||||
v-model:count-per-page="countPerPage" />
|
||||
</v-window-item>
|
||||
<v-window-item value="chart">
|
||||
|
||||
<explore-chart-tab ref="exploreChartTab"
|
||||
:loading="loading" />
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card>
|
||||
@@ -127,6 +128,7 @@
|
||||
<script setup lang="ts">
|
||||
import ExploreQueryTab from '@/views/desktop/insights/tabs/ExploreQueryTab.vue';
|
||||
import ExploreDataTableTab from '@/views/desktop/insights/tabs/ExploreDataTableTab.vue';
|
||||
import ExploreChartTab from '@/views/desktop/insights/tabs/ExploreChartTab.vue';
|
||||
import ExportDialog from '@/views/desktop/statistics/transaction/dialogs/ExportDialog.vue';
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
|
||||
@@ -180,6 +182,7 @@ const props = defineProps<InsightsExploreProps>();
|
||||
type ExplorePageTabType = 'query' | 'table' | 'chart';
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
type ExploreDataTableTabType = InstanceType<typeof ExploreDataTableTab>;
|
||||
type ExploreChartTabType = InstanceType<typeof ExploreChartTab>;
|
||||
type ExportDialogType = InstanceType<typeof ExportDialog>;
|
||||
|
||||
const router = useRouter();
|
||||
@@ -201,6 +204,7 @@ const exploresStore = useExploresStore();
|
||||
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
const exploreDataTableTab = useTemplateRef<ExploreDataTableTabType>('exploreDataTableTab');
|
||||
const exploreChartTab = useTemplateRef<ExploreChartTabType>('exploreChartTab');
|
||||
const exportDialog = useTemplateRef<ExportDialogType>('exportDialog');
|
||||
|
||||
const loading = ref<boolean>(true);
|
||||
@@ -234,6 +238,10 @@ const allTabs = computed<{ name: string, value: ExplorePageTabType }[]>(() => {
|
||||
{
|
||||
name: tt('Data Table'),
|
||||
value: 'table'
|
||||
},
|
||||
{
|
||||
name: tt('Chart'),
|
||||
value: 'chart'
|
||||
}
|
||||
];
|
||||
});
|
||||
@@ -333,6 +341,12 @@ function exportResults(): void {
|
||||
if (activeTab.value === 'table' && filteredTransactions.value) {
|
||||
const results = exploreDataTableTab.value?.buildExportResults();
|
||||
|
||||
if (results) {
|
||||
exportDialog.value?.open(results);
|
||||
}
|
||||
} else if (activeTab.value === 'chart') {
|
||||
const results = exploreChartTab.value?.buildExportResults();
|
||||
|
||||
if (results) {
|
||||
exportDialog.value?.open(results);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<v-card-text class="px-5 py-0 mb-4">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<div class="d-flex overflow-x-auto align-center gap-2 pt-2">
|
||||
<v-select
|
||||
class="flex-0-0"
|
||||
min-width="150"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
density="compact"
|
||||
:disabled="loading"
|
||||
:label="tt('Chart Type')"
|
||||
:items="allTransactionExploreChartTypes"
|
||||
:model-value="currentChartType"
|
||||
@update:model-value="updateChartType($event as TransactionExploreChartTypeValue)"
|
||||
/>
|
||||
<v-select
|
||||
class="flex-0-0"
|
||||
min-width="150"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
density="compact"
|
||||
:disabled="loading"
|
||||
:label="tt('Axis / Category')"
|
||||
:items="allTransactionExploreDataDimensions"
|
||||
:model-value="currentCategoryDimension"
|
||||
@update:model-value="updateCategoryDimension($event as TransactionExploreDataDimensionType)"
|
||||
/>
|
||||
<v-select
|
||||
class="flex-0-0"
|
||||
min-width="150"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
density="compact"
|
||||
:disabled="loading || !TransactionExploreChartType.valueOf(currentChartType)?.seriesDimensionRequired"
|
||||
:label="tt('Series')"
|
||||
:items="allTransactionExploreDataDimensions"
|
||||
:model-value="TransactionExploreChartType.valueOf(currentChartType)?.seriesDimensionRequired ? currentSeriesDimension : TransactionExploreDataDimension.None.value"
|
||||
@update:model-value="updateSeriesDimension($event as TransactionExploreDataDimensionType)"
|
||||
>
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item :disabled="item.value === currentCategoryDimension && item.value !== TransactionExploreDataDimension.SeriesDimensionDefault.value" v-bind="props">
|
||||
<template #title>
|
||||
<div class="text-truncate">{{ item.raw.name }}</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
<v-select
|
||||
class="flex-0-0"
|
||||
min-width="150"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
density="compact"
|
||||
:disabled="loading"
|
||||
:label="tt('Value Metric')"
|
||||
:items="allTransactionExploreValueMetrics"
|
||||
:model-value="currentValueMetric"
|
||||
@update:model-value="updateValueMetric($event as TransactionExploreValueMetricType)"
|
||||
/>
|
||||
<v-spacer class="flex-1-1"/>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text :class="{ 'readonly': loading }" v-if="currentChartType === TransactionExploreChartType.Pie.value">
|
||||
<pie-chart
|
||||
:items="[
|
||||
{id: '1', name: '---', value: 60, color: '7c7c7f'},
|
||||
{id: '2', name: '---', value: 20, color: 'a5a5aa'},
|
||||
{id: '3', name: '---', value: 20, color: 'c5c5c9'}
|
||||
]"
|
||||
:skeleton="true"
|
||||
id-field="id"
|
||||
name-field="name"
|
||||
value-field="value"
|
||||
color-field="color"
|
||||
v-if="loading"
|
||||
/>
|
||||
<pie-chart
|
||||
:items="categoryDimensionTransactionExploreData && categoryDimensionTransactionExploreData.length ? categoryDimensionTransactionExploreData : []"
|
||||
:min-valid-percent="0.0001"
|
||||
:show-value="true"
|
||||
:show-percent="true"
|
||||
:enable-click-item="true"
|
||||
:amount-value="exploresStore.transactionExploreFilter.valueMetric !== TransactionExploreValueMetric.TransactionCount.value"
|
||||
:default-currency="defaultCurrency"
|
||||
id-field="categoryId"
|
||||
name-field="categoryDisplayName"
|
||||
value-field="value"
|
||||
v-else-if="!loading"
|
||||
@click="onClickPieChartItem"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text :class="{ 'readonly': loading }" v-if="currentChartType === TransactionExploreChartType.Radar.value">
|
||||
<radar-chart
|
||||
:items="[
|
||||
{name: '---', value: 10},
|
||||
{name: '---', value: 10},
|
||||
{name: '---', value: 10},
|
||||
{name: '---', value: 10},
|
||||
{name: '---', value: 10},
|
||||
{name: '---', value: 10}
|
||||
]"
|
||||
:skeleton="true"
|
||||
name-field="name"
|
||||
value-field="value"
|
||||
v-if="loading"
|
||||
/>
|
||||
<radar-chart
|
||||
:items="categoryDimensionTransactionExploreData && categoryDimensionTransactionExploreData.length ? categoryDimensionTransactionExploreData : []"
|
||||
:min-valid-percent="0.0001"
|
||||
:show-value="true"
|
||||
:show-percent="true"
|
||||
:amount-value="exploresStore.transactionExploreFilter.valueMetric !== TransactionExploreValueMetric.TransactionCount.value"
|
||||
:default-currency="defaultCurrency"
|
||||
name-field="categoryDisplayName"
|
||||
value-field="value"
|
||||
v-else-if="!loading"
|
||||
/>
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { useUserStore } from '@/stores/user.ts';
|
||||
import {
|
||||
type CategoriedInfo,
|
||||
type SeriesedInfo,
|
||||
TransactionExploreDimensionType,
|
||||
useExploresStore
|
||||
} from '@/stores/explore.ts';
|
||||
|
||||
import { type NameValue } from '@/core/base.ts';
|
||||
import {
|
||||
TransactionExploreChartTypeValue,
|
||||
TransactionExploreChartType,
|
||||
TransactionExploreDataDimensionType,
|
||||
TransactionExploreDataDimension,
|
||||
TransactionExploreValueMetricType,
|
||||
TransactionExploreValueMetric
|
||||
} from '@/core/explore.ts';
|
||||
|
||||
import {
|
||||
isDefined
|
||||
} from '@/lib/common.ts';
|
||||
|
||||
import {
|
||||
parseDateTimeFromUnixTime
|
||||
} from '@/lib/datetime.ts';
|
||||
|
||||
interface InsightsExploreDataTableTabProps {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
interface CategoryDimensionData {
|
||||
categoryDisplayName: string;
|
||||
categoryId: string;
|
||||
categoryIdType: TransactionExploreDimensionType;
|
||||
value: number;
|
||||
}
|
||||
|
||||
defineProps<InsightsExploreDataTableTabProps>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
tt,
|
||||
getAllTransactionExploreDataDimensions,
|
||||
getAllTransactionExploreValueMetrics,
|
||||
getAllTransactionExploreChartTypes,
|
||||
formatDateTimeToShortDateTime,
|
||||
formatDateTimeToShortDate,
|
||||
formatDateTimeToGregorianLikeShortYear,
|
||||
formatDateTimeToGregorianLikeShortYearMonth,
|
||||
formatDateTimeToGregorianLikeYearQuarter,
|
||||
formatDateTimeToGregorianLikeFiscalYear,
|
||||
formatAmountToLocalizedNumerals,
|
||||
formatAmountToWesternArabicNumeralsWithoutDigitGrouping
|
||||
} = useI18n();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const exploresStore = useExploresStore();
|
||||
|
||||
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
|
||||
|
||||
const allTransactionExploreDataDimensions = computed<NameValue[]>(() => getAllTransactionExploreDataDimensions());
|
||||
const allTransactionExploreValueMetrics = computed<NameValue[]>(() => getAllTransactionExploreValueMetrics());
|
||||
const allTransactionExploreChartTypes = computed<NameValue[]>(() => getAllTransactionExploreChartTypes());
|
||||
|
||||
const currentCategoryDimension = computed<TransactionExploreDataDimensionType>(() => exploresStore.transactionExploreFilter.categoryDimension);
|
||||
const currentSeriesDimension = computed<TransactionExploreDataDimensionType>(() => exploresStore.transactionExploreFilter.seriesDimension);
|
||||
const currentValueMetric = computed<TransactionExploreValueMetricType>(() => exploresStore.transactionExploreFilter.valueMetric);
|
||||
const currentChartType = computed<TransactionExploreChartTypeValue>(() => exploresStore.transactionExploreFilter.chartType);
|
||||
|
||||
const categoryDimensionTransactionExploreData = computed<CategoryDimensionData[]>(() => {
|
||||
if (currentChartType.value !== TransactionExploreChartType.Pie.value && currentChartType.value !== TransactionExploreChartType.Radar.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!exploresStore.categoriedTransactionExploreData || !exploresStore.categoriedTransactionExploreData.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: CategoryDimensionData[] = [];
|
||||
|
||||
for (const categoriedData of exploresStore.categoriedTransactionExploreData) {
|
||||
const data = categoriedData.data[0];
|
||||
|
||||
if (!isDefined(data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const displayName = getCategoriedDataDisplayName(categoriedData);
|
||||
|
||||
result.push({
|
||||
categoryDisplayName: displayName,
|
||||
categoryId: categoriedData.categoryId,
|
||||
categoryIdType: categoriedData.categoryIdType,
|
||||
value: data.value
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
function getCategoriedDataDisplayName(info: CategoriedInfo | SeriesedInfo): string {
|
||||
let name: string = '';
|
||||
let needI18n: boolean | undefined = false;
|
||||
let i18nParameters: Record<string, unknown> | undefined = undefined;
|
||||
let dimessionType: TransactionExploreDataDimensionType = TransactionExploreDataDimension.None.value;
|
||||
|
||||
if ('categoryName' in info) {
|
||||
name = info.categoryName;
|
||||
needI18n = info.categoryNameNeedI18n;
|
||||
i18nParameters = info.categoryNameI18nParameters;
|
||||
dimessionType = exploresStore.transactionExploreFilter.categoryDimension;
|
||||
} else if ('seriesName' in info) {
|
||||
name = info.seriesName;
|
||||
needI18n = info.seriesNameNeedI18n;
|
||||
i18nParameters = info.seriesNameI18nParameters;
|
||||
dimessionType = exploresStore.transactionExploreFilter.seriesDimension;
|
||||
}
|
||||
|
||||
let displayName: string = name;
|
||||
|
||||
// convert the name to i18n if needed
|
||||
if (needI18n && i18nParameters) {
|
||||
displayName = tt(name, i18nParameters);
|
||||
} else if (needI18n && !i18nParameters) {
|
||||
displayName = tt(name);
|
||||
}
|
||||
|
||||
// convert the name to formatted date time if needed
|
||||
if (dimessionType === TransactionExploreDataDimension.DateTime.value) {
|
||||
displayName = formatDateTimeToShortDateTime(parseDateTimeFromUnixTime(parseInt(name)));
|
||||
} else if (dimessionType === TransactionExploreDataDimension.DateTimeByDay.value) {
|
||||
displayName = formatDateTimeToShortDate(parseDateTimeFromUnixTime(parseInt(name)));
|
||||
} else if (dimessionType === TransactionExploreDataDimension.DateTimeByMonth.value) {
|
||||
displayName = formatDateTimeToGregorianLikeShortYearMonth(parseDateTimeFromUnixTime(parseInt(name)));
|
||||
} else if (dimessionType === TransactionExploreDataDimension.DateTimeByQuarter.value) {
|
||||
displayName = formatDateTimeToGregorianLikeYearQuarter(parseDateTimeFromUnixTime(parseInt(name)));
|
||||
} else if (dimessionType === TransactionExploreDataDimension.DateTimeByYear.value) {
|
||||
displayName = formatDateTimeToGregorianLikeShortYear(parseDateTimeFromUnixTime(parseInt(name)));
|
||||
} else if (dimessionType === TransactionExploreDataDimension.DateTimeByFiscalYear.value) {
|
||||
displayName = formatDateTimeToGregorianLikeFiscalYear(parseDateTimeFromUnixTime(parseInt(name)));
|
||||
}
|
||||
|
||||
if (dimessionType === TransactionExploreDataDimension.SourceAmount.value
|
||||
|| dimessionType === TransactionExploreDataDimension.DestinationAmount.value) {
|
||||
if (name !== '' && name !== 'none' && Number.isFinite(parseInt(name))) {
|
||||
displayName = formatAmountToLocalizedNumerals(parseInt(name));
|
||||
}
|
||||
}
|
||||
|
||||
return displayName;
|
||||
}
|
||||
|
||||
function updateCategoryDimension(categoryDimension: TransactionExploreDataDimensionType): void {
|
||||
exploresStore.updateTransactionExploreFilter({
|
||||
categoryDimension: categoryDimension,
|
||||
});
|
||||
}
|
||||
|
||||
function updateSeriesDimension(seriesDimension: TransactionExploreDataDimensionType): void {
|
||||
exploresStore.updateTransactionExploreFilter({
|
||||
seriesDimension: seriesDimension,
|
||||
});
|
||||
}
|
||||
|
||||
function updateValueMetric(valueMetric: TransactionExploreValueMetricType): void {
|
||||
exploresStore.updateTransactionExploreFilter({
|
||||
valueMetric: valueMetric,
|
||||
});
|
||||
}
|
||||
|
||||
function updateChartType(chartType: TransactionExploreChartTypeValue): void {
|
||||
exploresStore.updateTransactionExploreFilter({
|
||||
chartType: chartType,
|
||||
});
|
||||
}
|
||||
|
||||
function onClickPieChartItem(item: Record<string, unknown>): void {
|
||||
if (!item || !('categoryId' in item) || !('categoryIdType' in item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = (item as unknown) as CategoryDimensionData;
|
||||
const params: string = exploresStore.getTransactionListPageParams(data.categoryIdType, data.categoryId);
|
||||
|
||||
if (params) {
|
||||
router.push(`/transaction/list?${params}`);
|
||||
}
|
||||
}
|
||||
|
||||
function buildExportResults(): { headers: string[], data: string[][] } | undefined {
|
||||
if (currentChartType.value === TransactionExploreChartType.Pie.value || currentChartType.value === TransactionExploreChartType.Radar.value) {
|
||||
const valueMetric = TransactionExploreValueMetric.valueOf(exploresStore.transactionExploreFilter.valueMetric);
|
||||
|
||||
return {
|
||||
headers: [
|
||||
tt('Name'),
|
||||
tt(valueMetric?.name ?? 'Unknown')
|
||||
],
|
||||
data: categoryDimensionTransactionExploreData.value.map(data => [
|
||||
data.categoryDisplayName,
|
||||
valueMetric?.isAmount ? formatAmountToWesternArabicNumeralsWithoutDigitGrouping(data.value) : data.value.toString(10)
|
||||
])
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
buildExportResults
|
||||
});
|
||||
</script>
|
||||
@@ -15,11 +15,11 @@
|
||||
<v-card border class="card-title-with-bg mt-4">
|
||||
<v-card-title class="d-flex align-center py-2 px-5">
|
||||
<v-icon :icon="mdiTextBoxSearchOutline" size="20" />
|
||||
<span class="query-name text-subtitle-1 ms-2" v-if="editingQuery !== query">{{ query.name || `${tt('Query')} #${queryIndex + 1}` }}</span>
|
||||
<span class="query-name text-subtitle-1 ms-2" v-if="editingQuery !== query">{{ query.name || tt('format.misc.queryIndex', { index: queryIndex + 1 }) }}</span>
|
||||
<div class="query-name-edit ms-2" v-if="editingQuery === query">
|
||||
<v-text-field autofocus type="text" density="compact" variant="underlined"
|
||||
:disabled="loading"
|
||||
:placeholder="`${tt('Query')} #${queryIndex + 1}`"
|
||||
:placeholder="tt('format.misc.queryIndex', { index: queryIndex + 1 })"
|
||||
v-text-field-auto-width="{ minWidth: 20, maxWidth: 300, auxSpanId: `query-name-aux-span-${queryIndex + 1}` }"
|
||||
v-model="editingQueryName"
|
||||
@keyup.esc="cancelUpdateQueryName"
|
||||
|
||||
@@ -267,6 +267,7 @@
|
||||
:show-value="showAmountInChart"
|
||||
:show-percent="showPercentInCategoricalChart"
|
||||
:enable-click-item="true"
|
||||
:amount-value="true"
|
||||
:default-currency="defaultCurrency"
|
||||
id-field="id"
|
||||
name-field="name"
|
||||
@@ -353,6 +354,7 @@
|
||||
:min-valid-percent="0.0001"
|
||||
:show-value="showAmountInChart"
|
||||
:show-percent="showPercentInCategoricalChart"
|
||||
:amount-value="true"
|
||||
:default-currency="defaultCurrency"
|
||||
name-field="name"
|
||||
value-field="totalAmount"
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
:show-center-text="true"
|
||||
:show-selected-item-info="true"
|
||||
:enable-click-item="true"
|
||||
:amount-value="true"
|
||||
:default-currency="defaultCurrency"
|
||||
class="statistics-pie-chart"
|
||||
name-field="name"
|
||||
|
||||
Reference in New Issue
Block a user