show total income and total expense in categorical overview chart

This commit is contained in:
MaysWind
2025-11-02 00:28:22 +08:00
parent b690316aa7
commit f3ccd3b66d
5 changed files with 523 additions and 316 deletions
@@ -14,33 +14,31 @@ import { useI18n } from '@/locales/helpers.ts';
import { useUserStore } from '@/stores/user.ts'; import { useUserStore } from '@/stores/user.ts';
import type { import {
SortableTransactionStatisticDataItem, type TransactionCategoricalOverviewAnalysisDataItem,
TransactionStatisticResponseItemWithInfo type TransactionCategoricalOverviewAnalysisDataItemOutflowItem,
TransactionCategoricalOverviewAnalysisDataItemType
} from '@/models/transaction.ts'; } from '@/models/transaction.ts';
import type { Account } from '@/models/account.ts';
import { values } from '@/core/base.ts'; import { values } from '@/core/base.ts';
import { ThemeType } from '@/core/theme.ts'; import { ThemeType } from '@/core/theme.ts';
import { CategoryType } from '@/core/category.ts';
import { TransactionRelatedAccountType } from '@/core/transaction.ts';
import { isNumber } from '@/lib/common.ts'; import { isNumber } from '@/lib/common.ts';
import { sortStatisticsItems } from '@/lib/statistics.ts';
import { getExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts'; import { getExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts';
enum SankeyChartDepth { enum SankeyChartDepth {
PrimaryIncomeCategory = 0, PrimaryIncomeCategory = 0,
SecondaryIncomeCategory = 1, SecondaryIncomeCategory = 1,
Account = 2, AccountForIncome = 2,
AccountWithTransfer = 3, AccountForExpense = 3,
SecondaryExpenseCategory = 4, SecondaryExpenseCategory = 4,
PrimaryExpenseCategory = 5 PrimaryExpenseCategory = 5
} }
enum SankeyChartNodeItemType { enum SankeyChartNodeItemType {
Account = 'account', Account = 'account',
Category = 'category' Category = 'category',
NetCashFlow = 'netCashFlow'
} }
interface SankeyChartData { interface SankeyChartData {
@@ -48,16 +46,20 @@ interface SankeyChartData {
links: SankeyChartLinkItem[]; links: SankeyChartLinkItem[];
} }
interface SankeyChartNodeItem extends SortableTransactionStatisticDataItem { interface SankeyChartNodeItem {
dateItemType: TransactionCategoricalOverviewAnalysisDataItemType;
itemType: SankeyChartNodeItemType; itemType: SankeyChartNodeItemType;
itemId: string; itemId: string;
name: string; name: string;
nameId: string;
displayName: string; displayName: string;
displayOrders: number[];
totalAmount: number; totalAmount: number;
accountNetCashFlow?: number;
percent?: number; percent?: number;
depth: number; depth: number;
itemStyle?: {
color?: string;
opacity?: number;
}
} }
interface SankeyChartLinkItem { interface SankeyChartLinkItem {
@@ -74,8 +76,7 @@ interface SankeyChartLinkItem {
const props = defineProps<{ const props = defineProps<{
skeleton?: boolean; skeleton?: boolean;
items: TransactionStatisticResponseItemWithInfo[]; items: TransactionCategoricalOverviewAnalysisDataItem[];
sortingType: number;
defaultCurrency?: string; defaultCurrency?: string;
enableClickItem?: boolean; enableClickItem?: boolean;
}>(); }>();
@@ -87,230 +88,134 @@ const emit = defineEmits<{
const theme = useTheme(); const theme = useTheme();
const { const {
tt,
formatAmountToLocalizedNumeralsWithCurrency, formatAmountToLocalizedNumeralsWithCurrency,
formatPercentToLocalizedNumerals formatPercentToLocalizedNumerals
} = useI18n(); } = useI18n();
const userStore = useUserStore(); const userStore = useUserStore();
const overviewDataItemTypeSankeyChartNodeItemTypeMap: Record<TransactionCategoricalOverviewAnalysisDataItemType, SankeyChartNodeItemType> = {
[TransactionCategoricalOverviewAnalysisDataItemType.IncomeByPrimaryCategory]: SankeyChartNodeItemType.Category,
[TransactionCategoricalOverviewAnalysisDataItemType.IncomeBySecondaryCategory]: SankeyChartNodeItemType.Category,
[TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount]: SankeyChartNodeItemType.Account,
[TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount]: SankeyChartNodeItemType.Account,
[TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow]: SankeyChartNodeItemType.NetCashFlow,
[TransactionCategoricalOverviewAnalysisDataItemType.ExpenseBySecondaryCategory]: SankeyChartNodeItemType.Category,
[TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByPrimaryCategory]: SankeyChartNodeItemType.Category
};
const overviewDataItemTypeSankeyChartNodeItemDepthMap: Record<TransactionCategoricalOverviewAnalysisDataItemType, number> = {
[TransactionCategoricalOverviewAnalysisDataItemType.IncomeByPrimaryCategory]: SankeyChartDepth.PrimaryIncomeCategory,
[TransactionCategoricalOverviewAnalysisDataItemType.IncomeBySecondaryCategory]: SankeyChartDepth.SecondaryIncomeCategory,
[TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount]: SankeyChartDepth.AccountForIncome,
[TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount]: SankeyChartDepth.AccountForExpense,
[TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow]: SankeyChartDepth.SecondaryExpenseCategory,
[TransactionCategoricalOverviewAnalysisDataItemType.ExpenseBySecondaryCategory]: SankeyChartDepth.SecondaryExpenseCategory,
[TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByPrimaryCategory]: SankeyChartDepth.PrimaryExpenseCategory
};
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark); const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
const sankeyData = computed<SankeyChartData>(() => { const sankeyData = computed<SankeyChartData>(() => {
const primaryIncomeCategoryNodesMap: Record<string, SankeyChartNodeItem> = {}; const nodes: SankeyChartNodeItem[] = [];
const secondaryIncomeCategoryNodesMap: Record<string, SankeyChartNodeItem> = {}; const links: SankeyChartLinkItem[] = [];
const incomeAccountNodesMap: Record<string, SankeyChartNodeItem> = {};
const expenseAccountNodesMap: Record<string, SankeyChartNodeItem> = {};
const secondaryExpenseCategoryNodesMap: Record<string, SankeyChartNodeItem> = {};
const primaryExpenseCategoryNodesMap: Record<string, SankeyChartNodeItem> = {};
const linksMap: Record<string, SankeyChartLinkItem> = {};
const accountsMap: Record<string, Account> = {};
for (const item of props.items) { for (const item of props.items) {
if (!item.primaryAccount || !item.account || !item.primaryCategory || !item.category || !item.amountInDefaultCurrency) { if (item.hidden) {
continue; continue;
} }
if (item.account.hidden || item.primaryAccount.hidden || item.category.hidden || item.primaryCategory.hidden) { const itemType = overviewDataItemTypeSankeyChartNodeItemTypeMap[item.type];
const depth = overviewDataItemTypeSankeyChartNodeItemDepthMap[item.type];
if (!itemType || itemType === SankeyChartNodeItemType.NetCashFlow || depth === undefined) {
continue; continue;
} }
if (item.relatedAccount && (item.relatedAccountType === TransactionRelatedAccountType.TransferFrom || item.relatedAccount.hidden || !item.relatedPrimaryAccount || item.relatedPrimaryAccount.hidden)) { if (item.totalAmount === 0 && item.outflows.length === 0) {
continue; continue;
} }
const incomeAccountNameId = `income_account:${item.account.id}`; const nodeItem: SankeyChartNodeItem = {
const expenseAccountNameId = `expense_account:${item.account.id}`; dateItemType: item.type,
accountsMap[item.account.id] = item.account; itemType: itemType,
itemId: item.id,
name: `${item.type}:${item.id}`,
displayName: item.name,
totalAmount: item.totalAmount,
percent: item.percent,
depth: depth
};
updateNodeItem(incomeAccountNodesMap, { if (!isNumber(nodeItem.percent) && nodeItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount) {
itemType: SankeyChartNodeItemType.Account, nodeItem.itemStyle = {
id: item.account.id, color: '#aaa',
name: item.account.name, opacity: 0.5
nameId: incomeAccountNameId, };
displayOrders: [item.account.displayOrder],
amount: item.primaryCategory.type == CategoryType.Income ? item.amountInDefaultCurrency : 0,
depth: SankeyChartDepth.Account
});
updateNodeItem(expenseAccountNodesMap, {
itemType: SankeyChartNodeItemType.Account,
id: item.account.id,
name: item.account.name,
nameId: expenseAccountNameId,
displayOrders: [item.account.displayOrder],
amount: item.primaryCategory.type == CategoryType.Expense ? item.amountInDefaultCurrency : 0,
depth: SankeyChartDepth.AccountWithTransfer
});
if (item.primaryCategory.type == CategoryType.Income) {
updateNodeItem(primaryIncomeCategoryNodesMap, {
itemType: SankeyChartNodeItemType.Category,
id: item.primaryCategory.id,
name: item.primaryCategory.name,
nameId: item.primaryCategory.id,
displayOrders: [item.primaryCategory.displayOrder],
amount: item.amountInDefaultCurrency,
depth: SankeyChartDepth.PrimaryIncomeCategory
});
updateNodeItem(secondaryIncomeCategoryNodesMap, {
itemType: SankeyChartNodeItemType.Category,
id: item.category.id,
name: item.category.name,
nameId: item.category.id,
displayOrders: [item.primaryCategory.displayOrder, item.category.displayOrder],
amount: item.amountInDefaultCurrency,
depth: SankeyChartDepth.SecondaryIncomeCategory
});
updateLinkItem(linksMap, {
sourceItemType: SankeyChartNodeItemType.Category,
sourceItemId: item.primaryCategory.id,
source: item.primaryCategory.id,
sourceName: item.primaryCategory.name,
targetItemType: SankeyChartNodeItemType.Category,
targetItemId: item.category.id,
target: item.category.id,
targetName: item.category.name,
value: item.amountInDefaultCurrency
});
updateLinkItem(linksMap, {
sourceItemType: SankeyChartNodeItemType.Category,
sourceItemId: item.category.id,
source: item.category.id,
sourceName: item.category.name,
targetItemType: SankeyChartNodeItemType.Account,
targetItemId: item.account.id,
target: incomeAccountNameId,
targetName: item.account.name,
value: item.amountInDefaultCurrency
});
} else if (item.primaryCategory.type == CategoryType.Expense) {
updateNodeItem(secondaryExpenseCategoryNodesMap, {
itemType: SankeyChartNodeItemType.Category,
id: item.category.id,
name: item.category.name,
nameId: item.category.id,
displayOrders: [item.primaryCategory.displayOrder, item.category.displayOrder],
amount: item.amountInDefaultCurrency,
depth: SankeyChartDepth.SecondaryExpenseCategory
});
updateNodeItem(primaryExpenseCategoryNodesMap, {
itemType: SankeyChartNodeItemType.Category,
id: item.primaryCategory.id,
name: item.primaryCategory.name,
nameId: item.primaryCategory.id,
displayOrders: [item.primaryCategory.displayOrder],
amount: item.amountInDefaultCurrency,
depth: SankeyChartDepth.PrimaryExpenseCategory
});
updateLinkItem(linksMap, {
sourceItemType: SankeyChartNodeItemType.Account,
sourceItemId: item.account.id,
source: expenseAccountNameId,
sourceName: item.account.name,
targetItemType: SankeyChartNodeItemType.Category,
targetItemId: item.category.id,
target: item.category.id,
targetName: item.category.name,
value: item.amountInDefaultCurrency
});
updateLinkItem(linksMap, {
sourceItemType: SankeyChartNodeItemType.Category,
sourceItemId: item.category.id,
source: item.category.id,
sourceName: item.category.name,
targetItemType: SankeyChartNodeItemType.Category,
targetItemId: item.primaryCategory.id,
target: item.primaryCategory.id,
targetName: item.primaryCategory.name,
value: item.amountInDefaultCurrency
});
} else if (item.primaryCategory.type == CategoryType.Transfer && item.relatedAccount) {
const relatedAccountNameId = `expense_account:${item.relatedAccount.id}`;
accountsMap[item.relatedAccount.id] = item.relatedAccount;
updateLinkItem(linksMap, {
sourceItemType: SankeyChartNodeItemType.Account,
sourceItemId: item.account.id,
source: incomeAccountNameId,
sourceName: item.account.name,
targetItemType: SankeyChartNodeItemType.Account,
targetItemId: item.relatedAccount.id,
target: relatedAccountNameId,
targetName: item.relatedAccount.name,
value: item.amountInDefaultCurrency
});
} }
}
for (const account of values(accountsMap)) { if (nodeItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount) {
const incomeAccountNameId = `income_account:${account.id}`; for (const outflowItem of item.outflows) {
const expenseAccountNameId = `expense_account:${account.id}`; if (outflowItem.relatedItem.type !== TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow) {
continue;
}
let totalOutflowAmount = 0; nodeItem.accountNetCashFlow = (nodeItem.accountNetCashFlow ?? 0) + outflowItem.amount;
let totalInflowAmount = 0;
for (const link of values(linksMap)) {
if (link.sourceItemType === SankeyChartNodeItemType.Account && link.sourceItemId === account.id) {
totalOutflowAmount += link.value;
} else if (link.targetItemType === SankeyChartNodeItemType.Account && link.targetItemId === account.id) {
totalInflowAmount += link.value;
} }
} }
const amountDifference = totalOutflowAmount - totalInflowAmount; nodes.push(nodeItem);
if (amountDifference > 0) { const combinedOutflows: Record<string, TransactionCategoricalOverviewAnalysisDataItemOutflowItem> = {};
updateNodeItem(incomeAccountNodesMap, {
itemType: SankeyChartNodeItemType.Account, for (const outflowItem of item.outflows) {
id: account.id, const relatedItem = outflowItem.relatedItem;
name: account.name,
nameId: incomeAccountNameId, if (!relatedItem) {
displayOrders: [account.displayOrder], continue;
amount: amountDifference, }
depth: SankeyChartDepth.AccountWithTransfer
}); if (outflowItem.relatedItem) {
} else if (amountDifference < 0) { const key = `${item.type}:${item.id}-${outflowItem.relatedItem.type}:${outflowItem.relatedItem.id}`;
updateNodeItem(expenseAccountNodesMap, { let combinedOutflow: TransactionCategoricalOverviewAnalysisDataItemOutflowItem | undefined = combinedOutflows[key];
itemType: SankeyChartNodeItemType.Account,
id: account.id, if (!combinedOutflow) {
name: account.name, combinedOutflow = {
nameId: expenseAccountNameId, relatedItem: outflowItem.relatedItem,
displayOrders: [account.displayOrder], amount: 0
amount: -amountDifference, };
depth: SankeyChartDepth.AccountWithTransfer combinedOutflows[key] = combinedOutflow;
}); }
combinedOutflow.amount += outflowItem.amount;
}
} }
if (Math.abs(amountDifference) > 0) { for (const outflowItem of values(combinedOutflows)) {
updateLinkItem(linksMap, { const relatedItem = outflowItem.relatedItem;
sourceItemType: SankeyChartNodeItemType.Account, const transferItemType = overviewDataItemTypeSankeyChartNodeItemTypeMap[relatedItem.type];
sourceItemId: account.id,
source: incomeAccountNameId, if (!transferItemType) {
sourceName: account.name, continue;
targetItemType: SankeyChartNodeItemType.Account, }
targetItemId: account.id,
target: expenseAccountNameId, const linkItem: SankeyChartLinkItem = {
targetName: account.name, sourceItemType: itemType,
value: Math.abs(amountDifference) sourceItemId: item.id,
}); source: `${item.type}:${item.id}`,
sourceDisplayName: item.name,
targetItemType: transferItemType,
targetItemId: relatedItem.id,
target: `${relatedItem.type}:${relatedItem.id}`,
targetDisplayName: relatedItem.name,
value: outflowItem.amount
};
links.push(linkItem);
} }
} }
const nodes: SankeyChartNodeItem[] = [];
const links: SankeyChartLinkItem[] = [];
addFinalSortedNodeItems(primaryIncomeCategoryNodesMap, nodes);
addFinalSortedNodeItems(secondaryIncomeCategoryNodesMap, nodes);
addFinalSortedNodeItems(incomeAccountNodesMap, nodes);
addFinalSortedNodeItems(expenseAccountNodesMap, nodes);
addFinalSortedNodeItems(secondaryExpenseCategoryNodesMap, nodes);
addFinalSortedNodeItems(primaryExpenseCategoryNodesMap, nodes);
addFinalLinkItems(linksMap, links);
const ret: SankeyChartData = { const ret: SankeyChartData = {
nodes: nodes, nodes: nodes,
links: links links: links
@@ -335,15 +240,44 @@ const chartOptions = computed<object>(() => {
const dataItem = params.data as SankeyChartNodeItem; const dataItem = params.data as SankeyChartNodeItem;
const value = dataItem.totalAmount; const value = dataItem.totalAmount;
const displayValue = formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency); const displayValue = formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency);
let displayTypeName = '';
if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.IncomeByPrimaryCategory) {
displayTypeName = tt('Income By Primary Category');
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.IncomeBySecondaryCategory) {
displayTypeName = tt('Income By Secondary Category');
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount) {
displayTypeName = tt('Income By Account');
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount) {
displayTypeName = tt('Expense By Account');
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow) {
displayTypeName = tt('Net Cash Flow');
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.ExpenseBySecondaryCategory) {
displayTypeName = tt('Expense By Secondary Category');
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByPrimaryCategory) {
displayTypeName = tt('Expense By Primary Category');
}
let tooltip = `<div><span>${dataItem.displayName}</span>`; let tooltip = `<div><span>${dataItem.displayName}</span>`;
if (isNumber(dataItem.percent)) { if (displayTypeName && (dataItem.dateItemType !== TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount || isNumber(dataItem.percent))) {
tooltip = `<div class="mb-1">${displayTypeName}</div>` + tooltip;
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount) {
tooltip = `<div class="mb-1">${tt('Account Balance')}</div>` + tooltip;
}
if (isNumber(dataItem.percent) && dataItem.percent > 0) {
const displayPercent = formatPercentToLocalizedNumerals(dataItem.percent, 2, '&lt;0.01'); const displayPercent = formatPercentToLocalizedNumerals(dataItem.percent, 2, '&lt;0.01');
tooltip += `<span class="ms-1" style="float: inline-end">(${displayPercent})</span>`; tooltip += `<span class="ms-1" style="float: inline-end">(${displayPercent})</span>`;
} }
tooltip += `<span class="ms-5" style="float: inline-end">${displayValue}</span></div>`; tooltip += `<span class="ms-5" style="float: inline-end">${displayValue}</span></div>`;
if (isNumber(dataItem.accountNetCashFlow) && dataItem.accountNetCashFlow !== 0) {
const displayAccountNetCashFlow = formatAmountToLocalizedNumeralsWithCurrency(dataItem.accountNetCashFlow, props.defaultCurrency);
tooltip += `<div class="mt-1"><span>${tt('Net Cash Flow')}</span><span class="ms-5" style="float: inline-end">${displayAccountNetCashFlow}</span></div>`;
}
return tooltip; return tooltip;
} else if (params.dataType === 'edge') { } else if (params.dataType === 'edge') {
const dataItem = params.data as SankeyChartLinkItem; const dataItem = params.data as SankeyChartLinkItem;
@@ -359,7 +293,7 @@ const chartOptions = computed<object>(() => {
{ {
type: 'sankey', type: 'sankey',
left: 10, left: 10,
top: 10, top: 0,
bottom: 10, bottom: 10,
roam: true, roam: true,
layoutIterations: 0, layoutIterations: 0,
@@ -393,7 +327,7 @@ const chartOptions = computed<object>(() => {
} }
}, },
{ {
depth: SankeyChartDepth.Account, depth: SankeyChartDepth.AccountForIncome,
itemStyle: { itemStyle: {
color: '#c07d43', color: '#c07d43',
opacity: 0.5 opacity: 0.5
@@ -404,7 +338,7 @@ const chartOptions = computed<object>(() => {
} }
}, },
{ {
depth: SankeyChartDepth.AccountWithTransfer, depth: SankeyChartDepth.AccountForExpense,
itemStyle: { itemStyle: {
color: '#c07d43', color: '#c07d43',
opacity: 0.5 opacity: 0.5
@@ -448,78 +382,6 @@ const chartOptions = computed<object>(() => {
}; };
}); });
function updateNodeItem(nodesMap: Record<string, SankeyChartNodeItem>, { itemType, id, name, nameId, displayOrders, amount, depth }: { itemType: SankeyChartNodeItemType, id: string, name: string, nameId: string, displayOrders: number[], amount: number, depth: number }) {
const node: SankeyChartNodeItem | undefined = nodesMap[nameId];
if (!node) {
nodesMap[nameId] = {
itemType: itemType,
itemId: id,
name: name,
nameId: nameId,
displayName: name,
displayOrders: displayOrders,
totalAmount: amount,
depth: depth
};
} else {
node.totalAmount += amount;
}
}
function updateLinkItem(linksMap: Record<string, SankeyChartLinkItem>, { sourceItemType, sourceItemId, source, sourceName, targetItemType, targetItemId, target, targetName, value }: { sourceItemType: SankeyChartNodeItemType, sourceItemId: string, source: string, sourceName: string, targetItemType: SankeyChartNodeItemType, targetItemId: string, target: string, targetName: string, value: number }) {
const key = `${source}:${target}`;
const link: SankeyChartLinkItem | undefined = linksMap[key];
if (!link) {
linksMap[key] = {
sourceItemType: sourceItemType,
sourceItemId: sourceItemId,
source: source,
sourceDisplayName: sourceName,
targetItemType: targetItemType,
targetItemId: targetItemId,
target: target,
targetDisplayName: targetName,
value: value
};
} else {
link.value += value;
}
}
function addFinalSortedNodeItems(nodesMap: Record<string, SankeyChartNodeItem>, allNodesArray: SankeyChartNodeItem[]): void {
const nodesArray: SankeyChartNodeItem[] = [];
let totalAmount = 0;
for (const node of values(nodesMap)) {
if (node.totalAmount > 0) {
totalAmount += node.totalAmount;
}
nodesArray.push(node);
}
sortStatisticsItems(nodesArray, props.sortingType);
for (const node of nodesArray) {
node.name = node.nameId;
node.percent = node.totalAmount > 0 && totalAmount > 0 ? (node.totalAmount / totalAmount) * 100 : undefined;
}
allNodesArray.push(...nodesArray);
}
function addFinalLinkItems(linksMap: Record<string, SankeyChartLinkItem>, allLinksArray: SankeyChartLinkItem[]): void {
const linksArray: SankeyChartLinkItem[] = [];
for (const link of values(linksMap)) {
linksArray.push(link);
}
allLinksArray.push(...linksArray);
}
function clickItem(e: ECElementEvent): void { function clickItem(e: ECElementEvent): void {
if (!props.enableClickItem || e.componentType !== 'series' || e.seriesType !=='sankey') { if (!props.enableClickItem || e.componentType !== 'series' || e.seriesType !=='sankey') {
return; return;
@@ -531,13 +393,22 @@ function clickItem(e: ECElementEvent): void {
if (e.dataType === 'node') { if (e.dataType === 'node') {
const dataItem = e.data as SankeyChartNodeItem; const dataItem = e.data as SankeyChartNodeItem;
if (dataItem.itemType === SankeyChartNodeItemType.NetCashFlow) {
return;
}
emit('click', dataItem.itemType, dataItem.itemId); emit('click', dataItem.itemType, dataItem.itemId);
} else if (e.dataType === 'edge') { } else if (e.dataType === 'edge') {
const dataItem = e.data as SankeyChartLinkItem; const dataItem = e.data as SankeyChartLinkItem;
if (dataItem.sourceItemType === SankeyChartNodeItemType.NetCashFlow) {
return;
}
if (dataItem.sourceItemType === dataItem.targetItemType && dataItem.sourceItemId === dataItem.targetItemId) { if (dataItem.sourceItemType === dataItem.targetItemType && dataItem.sourceItemId === dataItem.targetItemId) {
emit('click', dataItem.sourceItemType, dataItem.sourceItemId); emit('click', dataItem.sourceItemType, dataItem.sourceItemId);
} else { } else if (dataItem.targetItemType !== SankeyChartNodeItemType.NetCashFlow) {
emit('click', dataItem.sourceItemType, dataItem.sourceItemId, dataItem.targetItemType, dataItem.targetItemId); emit('click', dataItem.sourceItemType, dataItem.sourceItemId, dataItem.targetItemType, dataItem.targetItemId);
} }
} }
+35
View File
@@ -762,6 +762,41 @@ export interface TransactionStatisticDataItemBase extends SortableTransactionSta
readonly totalAmount: number; readonly totalAmount: number;
} }
export interface TransactionCategoricalOverviewAnalysisData {
readonly totalIncome: number;
readonly totalExpense: number;
readonly items: TransactionCategoricalOverviewAnalysisDataItem[];
}
export enum TransactionCategoricalOverviewAnalysisDataItemType {
IncomeByPrimaryCategory = 'incomeByPrimaryCategory',
IncomeBySecondaryCategory = 'incomeBySecondaryCategory',
IncomeByAccount = 'incomeByAccount',
ExpenseByAccount = 'expenseByAccount',
NetCashFlow = 'netCashFlow',
ExpenseBySecondaryCategory = 'expenseBySecondaryCategory',
ExpenseByPrimaryCategory = 'expenseByPrimaryCategory'
}
export interface TransactionCategoricalOverviewAnalysisDataItem extends SortableTransactionStatisticDataItem {
readonly id: string;
readonly name: string;
readonly type: TransactionCategoricalOverviewAnalysisDataItemType;
readonly displayOrders: number[];
readonly hidden: boolean;
readonly inflows: TransactionCategoricalOverviewAnalysisDataItemOutflowItem[];
readonly outflows: TransactionCategoricalOverviewAnalysisDataItemOutflowItem[];
totalAmount: number;
totalNonNegativeAmount: number;
includeInPercent?: boolean;
percent?: number;
}
export interface TransactionCategoricalOverviewAnalysisDataItemOutflowItem {
readonly relatedItem: TransactionCategoricalOverviewAnalysisDataItem;
amount: number;
}
export interface TransactionCategoricalAnalysisData { export interface TransactionCategoricalAnalysisData {
readonly totalAmount: number; readonly totalAmount: number;
readonly items: TransactionCategoricalAnalysisDataItem[]; readonly items: TransactionCategoricalAnalysisDataItem[];
+303 -24
View File
@@ -28,20 +28,23 @@ import {
import { DEFAULT_ACCOUNT_ICON, DEFAULT_CATEGORY_ICON } from '@/consts/icon.ts'; import { DEFAULT_ACCOUNT_ICON, DEFAULT_CATEGORY_ICON } from '@/consts/icon.ts';
import { DEFAULT_ACCOUNT_COLOR, DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts'; import { DEFAULT_ACCOUNT_COLOR, DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts';
import type { import {
TransactionStatisticResponse, type TransactionStatisticResponse,
TransactionStatisticResponseItem, type TransactionStatisticResponseItem,
TransactionStatisticTrendsResponseItem, type TransactionStatisticTrendsResponseItem,
TransactionStatisticResponseItemWithInfo, type TransactionStatisticResponseItemWithInfo,
TransactionStatisticResponseWithInfo, type TransactionStatisticResponseWithInfo,
TransactionStatisticTrendsResponseItemWithInfo, type TransactionStatisticTrendsResponseItemWithInfo,
TransactionStatisticDataItemType, type TransactionStatisticDataItemType,
TransactionStatisticDataItemBase, type TransactionStatisticDataItemBase,
TransactionCategoricalAnalysisData, type TransactionCategoricalOverviewAnalysisData,
TransactionCategoricalAnalysisDataItem, type TransactionCategoricalOverviewAnalysisDataItem,
TransactionTrendsAnalysisData, type TransactionCategoricalAnalysisData,
TransactionTrendsAnalysisDataItem, type TransactionCategoricalAnalysisDataItem,
TransactionTrendsAnalysisDataAmount type TransactionTrendsAnalysisData,
type TransactionTrendsAnalysisDataItem,
type TransactionTrendsAnalysisDataAmount,
TransactionCategoricalOverviewAnalysisDataItemType
} from '@/models/transaction.ts'; } from '@/models/transaction.ts';
import { import {
@@ -203,36 +206,276 @@ export const useStatisticsStore = defineStore('statistics', () => {
return getCategoryTotalAmountItems(transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items, transactionStatisticsFilter.value); return getCategoryTotalAmountItems(transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items, transactionStatisticsFilter.value);
}); });
const categoricalAllAnalysisData = computed<TransactionStatisticResponseWithInfo | null>(() => { const categoricalOverviewAnalysisData = computed<TransactionCategoricalOverviewAnalysisData | null>(() => {
if (!transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value || !transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items) { if (!transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value || !transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items) {
return null; return null;
} }
const allDataItems: TransactionStatisticResponseItemWithInfo[] = []; const allDataItemsMap: Record<string, TransactionCategoricalOverviewAnalysisDataItem> = {};
const allIncomeByPrimaryCategoryDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allIncomeBySecondaryCategoryDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allIncomeByAccountDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allExpenseByAccountDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allExpenseBySecondaryCategoryDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allExpenseByPrimaryCategoryDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allOpeningBalanceDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allNetCashFlowDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
let totalIncome: number = 0;
let totalExpense: number = 0;
for (const item of transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items) { for (const item of transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items) {
if (!item.primaryAccount || !item.account || !item.primaryCategory || !item.category) { if (!item.primaryAccount || !item.account || !item.primaryCategory || !item.category) {
continue; continue;
} }
if (item.relatedAccount && item.relatedAccountType === TransactionRelatedAccountType.TransferFrom) {
continue;
}
if (!isNumber(item.amountInDefaultCurrency)) {
continue;
}
if (transactionStatisticsFilter.value.filterAccountIds && transactionStatisticsFilter.value.filterAccountIds[item.account.id]) { if (transactionStatisticsFilter.value.filterAccountIds && transactionStatisticsFilter.value.filterAccountIds[item.account.id]) {
continue; continue;
} }
if (transactionStatisticsFilter.value.filterAccountIds && item.relatedAccount && transactionStatisticsFilter.value.filterAccountIds[item.relatedAccount.id]) {
continue;
}
if (transactionStatisticsFilter.value.filterCategoryIds && transactionStatisticsFilter.value.filterCategoryIds[item.category.id]) { if (transactionStatisticsFilter.value.filterCategoryIds && transactionStatisticsFilter.value.filterCategoryIds[item.category.id]) {
continue; continue;
} }
allDataItems.push(item); if (item.category.type === CategoryType.Income) {
totalIncome += item.amountInDefaultCurrency;
} else if (item.category.type === CategoryType.Expense) {
totalExpense += item.amountInDefaultCurrency;
}
const incomeByAccountKey = `${TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount}:${item.account.id}`;
const expenseByAccountKey = `${TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount}:${item.account.id}`;
let incomeByAccountItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[incomeByAccountKey];
let expenseByAccountItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[expenseByAccountKey];
if (!incomeByAccountItem) {
incomeByAccountItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.account.id,
item.account.name,
TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount,
[item.primaryAccount.category, item.primaryAccount.displayOrder, item.account.displayOrder],
item.primaryAccount.hidden || item.account.hidden);
allDataItemsMap[incomeByAccountKey] = incomeByAccountItem;
allIncomeByAccountDataItems.push(incomeByAccountItem);
}
if (!expenseByAccountItem) {
expenseByAccountItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.account.id,
item.account.name,
TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount,
[item.primaryAccount.category, item.primaryAccount.displayOrder, item.account.displayOrder],
item.primaryAccount.hidden || item.account.hidden);
allDataItemsMap[expenseByAccountKey] = expenseByAccountItem;
allExpenseByAccountDataItems.push(expenseByAccountItem);
}
if (item.category.type === CategoryType.Income) {
const primaryCategoryItemKey = `${TransactionCategoricalOverviewAnalysisDataItemType.IncomeByPrimaryCategory}:${item.primaryCategory.id}`;
const secondaryCategoryItemKey = `${TransactionCategoricalOverviewAnalysisDataItemType.IncomeBySecondaryCategory}:${item.category.id}`;
let primaryCategoryDataItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[primaryCategoryItemKey];
let secondaryCategoryDataItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[secondaryCategoryItemKey];
if (!primaryCategoryDataItem) {
primaryCategoryDataItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.primaryCategory.id,
item.primaryCategory.name,
TransactionCategoricalOverviewAnalysisDataItemType.IncomeByPrimaryCategory,
[item.primaryCategory.displayOrder],
item.primaryCategory.hidden);
allDataItemsMap[primaryCategoryItemKey] = primaryCategoryDataItem;
allIncomeByPrimaryCategoryDataItems.push(primaryCategoryDataItem);
}
if (!secondaryCategoryDataItem) {
secondaryCategoryDataItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.category.id,
item.category.name,
TransactionCategoricalOverviewAnalysisDataItemType.IncomeBySecondaryCategory,
[item.primaryCategory.displayOrder, item.category.displayOrder],
item.primaryCategory.hidden || item.category.hidden);
allDataItemsMap[secondaryCategoryItemKey] = secondaryCategoryDataItem;
allIncomeBySecondaryCategoryDataItems.push(secondaryCategoryDataItem);
}
primaryCategoryDataItem.totalAmount += item.amountInDefaultCurrency;
primaryCategoryDataItem.totalNonNegativeAmount += item.amountInDefaultCurrency > 0 ? item.amountInDefaultCurrency : 0;
primaryCategoryDataItem.includeInPercent = true;
primaryCategoryDataItem.outflows.push({ amount: item.amountInDefaultCurrency, relatedItem: secondaryCategoryDataItem });
secondaryCategoryDataItem.totalAmount += item.amountInDefaultCurrency;
secondaryCategoryDataItem.totalNonNegativeAmount += item.amountInDefaultCurrency > 0 ? item.amountInDefaultCurrency : 0;
secondaryCategoryDataItem.includeInPercent = true;
secondaryCategoryDataItem.inflows.push({ amount: item.amountInDefaultCurrency, relatedItem: primaryCategoryDataItem });
secondaryCategoryDataItem.outflows.push({ amount: item.amountInDefaultCurrency, relatedItem: incomeByAccountItem });
incomeByAccountItem.totalAmount += item.amountInDefaultCurrency;
incomeByAccountItem.totalNonNegativeAmount += item.amountInDefaultCurrency > 0 ? item.amountInDefaultCurrency : 0;
incomeByAccountItem.includeInPercent = true;
incomeByAccountItem.inflows.push({ amount: item.amountInDefaultCurrency, relatedItem: secondaryCategoryDataItem });
} else if (item.category.type === CategoryType.Expense) {
const primaryCategoryItemKey = `${TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByPrimaryCategory}:${item.primaryCategory.id}`;
const secondaryCategoryItemKey = `${TransactionCategoricalOverviewAnalysisDataItemType.ExpenseBySecondaryCategory}:${item.category.id}`;
let primaryCategoryDataItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[primaryCategoryItemKey];
let secondaryCategoryDataItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[secondaryCategoryItemKey];
if (!primaryCategoryDataItem) {
primaryCategoryDataItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.primaryCategory.id,
item.primaryCategory.name,
TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByPrimaryCategory,
[item.primaryCategory.displayOrder],
item.primaryCategory.hidden);
allDataItemsMap[primaryCategoryItemKey] = primaryCategoryDataItem;
allExpenseByPrimaryCategoryDataItems.push(primaryCategoryDataItem);
}
if (!secondaryCategoryDataItem) {
secondaryCategoryDataItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.category.id,
item.category.name,
TransactionCategoricalOverviewAnalysisDataItemType.ExpenseBySecondaryCategory,
[item.primaryCategory.displayOrder, item.category.displayOrder],
item.primaryCategory.hidden || item.category.hidden);
allDataItemsMap[secondaryCategoryItemKey] = secondaryCategoryDataItem;
allExpenseBySecondaryCategoryDataItems.push(secondaryCategoryDataItem);
}
expenseByAccountItem.totalAmount += item.amountInDefaultCurrency;
expenseByAccountItem.totalNonNegativeAmount += item.amountInDefaultCurrency > 0 ? item.amountInDefaultCurrency : 0;
expenseByAccountItem.includeInPercent = true;
expenseByAccountItem.outflows.push({ amount: item.amountInDefaultCurrency, relatedItem: secondaryCategoryDataItem });
secondaryCategoryDataItem.totalAmount += item.amountInDefaultCurrency;
secondaryCategoryDataItem.totalNonNegativeAmount += item.amountInDefaultCurrency > 0 ? item.amountInDefaultCurrency : 0
secondaryCategoryDataItem.includeInPercent = true;
secondaryCategoryDataItem.inflows.push({ amount: item.amountInDefaultCurrency, relatedItem: expenseByAccountItem });
secondaryCategoryDataItem.outflows.push({ amount: item.amountInDefaultCurrency, relatedItem: primaryCategoryDataItem });
primaryCategoryDataItem.totalAmount += item.amountInDefaultCurrency;
primaryCategoryDataItem.totalNonNegativeAmount += item.amountInDefaultCurrency > 0 ? item.amountInDefaultCurrency : 0;
primaryCategoryDataItem.includeInPercent = true;
primaryCategoryDataItem.inflows.push({ amount: item.amountInDefaultCurrency, relatedItem: secondaryCategoryDataItem });
} else if (item.category.type === CategoryType.Transfer && item.relatedPrimaryAccount && item.relatedAccount) {
const transferToAccountKey = `${TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount}:${item.relatedAccount.id}`;
let transferToAccountItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[transferToAccountKey];
if (!transferToAccountItem) {
transferToAccountItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.relatedAccount.id,
item.relatedAccount.name,
TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount,
[item.relatedPrimaryAccount.category, item.relatedPrimaryAccount.displayOrder, item.relatedAccount.displayOrder],
item.relatedPrimaryAccount.hidden || item.relatedAccount.hidden);
allDataItemsMap[transferToAccountKey] = transferToAccountItem;
allExpenseByAccountDataItems.push(transferToAccountItem);
}
incomeByAccountItem.outflows.push({ amount: item.amountInDefaultCurrency, relatedItem: transferToAccountItem });
transferToAccountItem.inflows.push({ amount: item.amountInDefaultCurrency, relatedItem: incomeByAccountItem });
}
} }
sortCategoricalOverviewAnalysisDataItems(allIncomeByPrimaryCategoryDataItems, transactionStatisticsFilter.value);
sortCategoricalOverviewAnalysisDataItems(allIncomeBySecondaryCategoryDataItems, transactionStatisticsFilter.value);
sortCategoricalOverviewAnalysisDataItems(allIncomeByAccountDataItems, transactionStatisticsFilter.value);
sortCategoricalOverviewAnalysisDataItems(allExpenseByAccountDataItems, transactionStatisticsFilter.value);
sortCategoricalOverviewAnalysisDataItems(allExpenseBySecondaryCategoryDataItems, transactionStatisticsFilter.value);
sortCategoricalOverviewAnalysisDataItems(allExpenseByPrimaryCategoryDataItems, transactionStatisticsFilter.value);
for (const item of allExpenseByAccountDataItems) {
const incomeByAccountKey = `${TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount}:${item.id}`;
const incomeByAccountItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[incomeByAccountKey];
let accountTotalInflowsAmount: number = 0;
let accountTotalIncomeAmount: number = 0;
let accountTotalTransferAmount: number = 0;
let accountTotalOutflowsAmount: number = 0;
if (incomeByAccountItem) {
for (const inflow of incomeByAccountItem.inflows) {
accountTotalInflowsAmount += inflow.amount;
accountTotalIncomeAmount += inflow.amount;
}
for (const outflow of incomeByAccountItem.outflows) {
accountTotalTransferAmount += outflow.amount;
}
}
for (const inflow of item.inflows) {
if (inflow.relatedItem.type === item.type && inflow.relatedItem.id === item.id) {
continue;
}
accountTotalInflowsAmount += inflow.amount;
}
for (const outflow of item.outflows) {
accountTotalOutflowsAmount += outflow.amount;
}
const accountBalance: number = accountTotalIncomeAmount - accountTotalTransferAmount - accountTotalOutflowsAmount;
const accountNetCashFlow: number = accountTotalInflowsAmount - accountTotalTransferAmount - accountTotalOutflowsAmount;
if (incomeByAccountItem && accountsStore.allAccountsMap[item.id]?.isAsset) {
if (accountBalance > 0) { // has positive balance, transfer the amount from income account to expense account
incomeByAccountItem.outflows.push({ amount: accountBalance + accountTotalOutflowsAmount, relatedItem: item });
item.inflows.push({ amount: accountBalance + accountTotalOutflowsAmount, relatedItem: incomeByAccountItem });
} else if (accountNetCashFlow < 0) { // has negative net cash flow, add the difference to income account
incomeByAccountItem.totalAmount += -accountNetCashFlow;
incomeByAccountItem.totalNonNegativeAmount += -accountNetCashFlow > 0 ? -accountNetCashFlow : 0;
incomeByAccountItem.outflows.push({ amount: -accountNetCashFlow, relatedItem: item });
item.inflows.push({ amount: -accountNetCashFlow, relatedItem: incomeByAccountItem });
}
}
if (accountNetCashFlow > 0) {
let netCashFlowItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow];
if (!netCashFlowItem) {
netCashFlowItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow,
'Net Cash Flow',
TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow,
[Number.MAX_SAFE_INTEGER],
false);
allDataItemsMap[TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow] = netCashFlowItem;
allNetCashFlowDataItems.push(netCashFlowItem);
}
item.outflows.push({ amount: accountNetCashFlow, relatedItem: netCashFlowItem });
netCashFlowItem.totalAmount += accountNetCashFlow;
netCashFlowItem.totalNonNegativeAmount += accountNetCashFlow > 0 ? accountNetCashFlow : 0;
netCashFlowItem.inflows.push({ amount: accountNetCashFlow, relatedItem: item });
}
}
const allDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [
...allIncomeByPrimaryCategoryDataItems,
...allIncomeBySecondaryCategoryDataItems,
...allIncomeByAccountDataItems,
...allOpeningBalanceDataItems,
...allExpenseByAccountDataItems,
...allExpenseBySecondaryCategoryDataItems,
...allNetCashFlowDataItems,
...allExpenseByPrimaryCategoryDataItems
];
return { return {
startTime: transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.startTime, totalIncome: totalIncome,
endTime: transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.endTime, totalExpense: totalExpense,
items: allDataItems items: allDataItems
}; };
}); });
@@ -466,6 +709,42 @@ export const useStatisticsStore = defineStore('statistics', () => {
return trendsData; return trendsData;
}); });
function createNewTransactionCategoricalOverviewAnalysisDataItem(id: string, name: string, type: TransactionCategoricalOverviewAnalysisDataItemType, displayOrders: number[], hidden: boolean): TransactionCategoricalOverviewAnalysisDataItem {
const dataItem: TransactionCategoricalOverviewAnalysisDataItem = {
id: id,
name: name,
type: type,
displayOrders: displayOrders,
hidden: hidden,
inflows: [],
outflows: [],
totalAmount: 0,
totalNonNegativeAmount: 0
};
return dataItem;
}
function sortCategoricalOverviewAnalysisDataItems(items: TransactionCategoricalOverviewAnalysisDataItem[], transactionStatisticsFilter: TransactionStatisticsFilter): void {
let totalNonNegativeAmount: number = 0;
for (const item of items) {
totalNonNegativeAmount += item.totalNonNegativeAmount;
}
if (totalNonNegativeAmount > 0) {
for (const item of items) {
if (!item.includeInPercent) {
continue;
}
item.percent = item.totalAmount * 100 / totalNonNegativeAmount;
}
}
sortStatisticsItems(items, transactionStatisticsFilter.sortingType);
}
function assembleAccountAndCategoryInfo(items: TransactionStatisticResponseItem[]): TransactionStatisticResponseItemWithInfo[] { function assembleAccountAndCategoryInfo(items: TransactionStatisticResponseItem[]): TransactionStatisticResponseItemWithInfo[] {
const finalItems: TransactionStatisticResponseItemWithInfo[] = []; const finalItems: TransactionStatisticResponseItemWithInfo[] = [];
const defaultCurrency = userStore.currentUserDefaultCurrency; const defaultCurrency = userStore.currentUserDefaultCurrency;
@@ -1289,7 +1568,7 @@ export const useStatisticsStore = defineStore('statistics', () => {
transactionStatisticsStateInvalid, transactionStatisticsStateInvalid,
// computed states // computed states
categoricalAnalysisChartDataCategory, categoricalAnalysisChartDataCategory,
categoricalAllAnalysisData, categoricalOverviewAnalysisData,
categoricalAnalysisData, categoricalAnalysisData,
trendsAnalysisData, trendsAnalysisData,
// functions // functions
@@ -20,7 +20,7 @@ import {
import { DISPLAY_HIDDEN_AMOUNT } from '@/consts/numeral.ts'; import { DISPLAY_HIDDEN_AMOUNT } from '@/consts/numeral.ts';
import type { import type {
TransactionStatisticResponseWithInfo, TransactionCategoricalOverviewAnalysisData,
TransactionCategoricalAnalysisData, TransactionCategoricalAnalysisData,
TransactionCategoricalAnalysisDataItem, TransactionCategoricalAnalysisDataItem,
TransactionTrendsAnalysisData TransactionTrendsAnalysisData
@@ -249,8 +249,8 @@ export function useStatisticsTransactionPageBase() {
query.value.chartDataType === ChartDataType.NetIncome.type; query.value.chartDataType === ChartDataType.NetIncome.type;
}); });
const categoricalOverviewAnalysisData = computed<TransactionCategoricalOverviewAnalysisData | null>(() => statisticsStore.categoricalOverviewAnalysisData);
const categoricalAnalysisData = computed<TransactionCategoricalAnalysisData>(() => statisticsStore.categoricalAnalysisData); const categoricalAnalysisData = computed<TransactionCategoricalAnalysisData>(() => statisticsStore.categoricalAnalysisData);
const categoricalAllAnalysisData = computed<TransactionStatisticResponseWithInfo | null>(() => statisticsStore.categoricalAllAnalysisData);
const trendsAnalysisData = computed<TransactionTrendsAnalysisData | null>(() => statisticsStore.trendsAnalysisData); const trendsAnalysisData = computed<TransactionTrendsAnalysisData | null>(() => statisticsStore.trendsAnalysisData);
function canShowCustomDateRange(dateRangeType: number): boolean { function canShowCustomDateRange(dateRangeType: number): boolean {
@@ -323,8 +323,8 @@ export function useStatisticsTransactionPageBase() {
showTotalAmountInTrendsChart, showTotalAmountInTrendsChart,
showStackedInTrendsChart, showStackedInTrendsChart,
translateNameInTrendsChart, translateNameInTrendsChart,
categoricalOverviewAnalysisData,
categoricalAnalysisData, categoricalAnalysisData,
categoricalAllAnalysisData,
trendsAnalysisData, trendsAnalysisData,
// functions // functions
canShowCustomDateRange, canShowCustomDateRange,
@@ -172,7 +172,27 @@
</template> </template>
<v-card-text class="statistics-overview-title pt-0" :class="{ 'disabled': loading }" <v-card-text class="statistics-overview-title pt-0" :class="{ 'disabled': loading }"
v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && (initing || (categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length))"> v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && isQuerySpecialChartType && queryChartDataType === ChartDataType.Overview.type && (initing || categoricalOverviewAnalysisData && categoricalOverviewAnalysisData.items && categoricalOverviewAnalysisData.items.length)">
<span class="statistics-subtitle">{{ tt('Total Income') }}</span>
<span class="statistics-overview-amount ms-3 text-income"
v-if="!initing && categoricalOverviewAnalysisData && categoricalOverviewAnalysisData.items && categoricalOverviewAnalysisData.items.length">
{{ getDisplayAmount(categoricalOverviewAnalysisData.totalIncome, defaultCurrency) }}
</span>
<v-skeleton-loader class="skeleton-no-margin ms-3 mb-2"
width="120px" type="text" :loading="true"
v-else-if="initing"></v-skeleton-loader>
<span class="statistics-subtitle ms-3">{{ tt('Total Expense') }}</span>
<span class="statistics-overview-amount ms-3 text-expense"
v-if="!initing && categoricalOverviewAnalysisData && categoricalOverviewAnalysisData.items && categoricalOverviewAnalysisData.items.length">
{{ getDisplayAmount(categoricalOverviewAnalysisData.totalExpense, defaultCurrency) }}
</span>
<v-skeleton-loader class="skeleton-no-margin ms-3 mb-2"
width="120px" type="text" :loading="true"
v-else-if="initing"></v-skeleton-loader>
</v-card-text>
<v-card-text class="statistics-overview-title pt-0" :class="{ 'disabled': loading }"
v-else-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && (initing || (categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length))">
<span class="statistics-subtitle">{{ totalAmountName }}</span> <span class="statistics-subtitle">{{ totalAmountName }}</span>
<span class="statistics-overview-amount ms-3" <span class="statistics-overview-amount ms-3"
:class="statisticsTextColor" :class="statisticsTextColor"
@@ -185,8 +205,11 @@
</v-card-text> </v-card-text>
<v-card-text class="statistics-overview-title pt-0" <v-card-text class="statistics-overview-title pt-0"
v-else-if="!initing && ((queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && (!categoricalAnalysisData || !categoricalAnalysisData.items || !categoricalAnalysisData.items.length)) v-else-if="!initing && (
|| (queryAnalysisType === StatisticsAnalysisType.TrendAnalysis && (!trendsAnalysisData || !trendsAnalysisData.items || !trendsAnalysisData.items.length)))"> (queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && isQuerySpecialChartType && queryChartDataType === ChartDataType.Overview.type && (!categoricalOverviewAnalysisData || !categoricalOverviewAnalysisData.items || !categoricalOverviewAnalysisData.items.length))
|| (queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && (!categoricalAnalysisData || !categoricalAnalysisData.items || !categoricalAnalysisData.items.length))
|| (queryAnalysisType === StatisticsAnalysisType.TrendAnalysis && (!trendsAnalysisData || !trendsAnalysisData.items || !trendsAnalysisData.items.length))
)">
<span class="statistics-subtitle statistics-overview-empty-tip">{{ tt('No transaction data') }}</span> <span class="statistics-subtitle statistics-overview-empty-tip">{{ tt('No transaction data') }}</span>
</v-card-text> </v-card-text>
@@ -198,8 +221,7 @@
v-if="initing" v-if="initing"
/> />
<account-and-category-sankey-chart <account-and-category-sankey-chart
:items="categoricalAllAnalysisData && categoricalAllAnalysisData.items && categoricalAllAnalysisData.items.length ? categoricalAllAnalysisData.items : []" :items="categoricalOverviewAnalysisData && categoricalOverviewAnalysisData.items && categoricalOverviewAnalysisData.items.length ? categoricalOverviewAnalysisData.items : []"
:sorting-type="querySortingType"
:enable-click-item="true" :enable-click-item="true"
:default-currency="defaultCurrency" :default-currency="defaultCurrency"
v-else-if="!initing" v-else-if="!initing"
@@ -526,8 +548,8 @@ const {
showTotalAmountInTrendsChart, showTotalAmountInTrendsChart,
showStackedInTrendsChart, showStackedInTrendsChart,
translateNameInTrendsChart, translateNameInTrendsChart,
categoricalOverviewAnalysisData,
categoricalAnalysisData, categoricalAnalysisData,
categoricalAllAnalysisData,
trendsAnalysisData, trendsAnalysisData,
canShowCustomDateRange, canShowCustomDateRange,
getTransactionCategoricalAnalysisDataItemDisplayColor, getTransactionCategoricalAnalysisDataItemDisplayColor,