mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-17 08:14:25 +08:00
add trend in income and expense card in overview page
This commit is contained in:
@@ -290,7 +290,7 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (interface{
|
|||||||
return nil, errs.ErrQueryItemsEmpty
|
return nil, errs.ErrQueryItemsEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(requestItems) > 5 {
|
if len(requestItems) > 10 {
|
||||||
log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] parse request failed, because there are too many items")
|
log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] parse request failed, because there are too many items")
|
||||||
return nil, errs.ErrQueryItemsTooMuch
|
return nil, errs.ErrQueryItemsTooMuch
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="256px" height="256px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|
||||||
|
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -1,3 +1,18 @@
|
|||||||
|
const allMonthsArray = [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December'
|
||||||
|
];
|
||||||
|
|
||||||
const allWeekDays = {
|
const allWeekDays = {
|
||||||
Sunday: {
|
Sunday: {
|
||||||
type: 0,
|
type: 0,
|
||||||
@@ -190,6 +205,7 @@ const defaultDateTimeFormatValue = 0;
|
|||||||
export default {
|
export default {
|
||||||
allWeekDays: allWeekDays,
|
allWeekDays: allWeekDays,
|
||||||
allWeekDaysArray: allWeekDaysArray,
|
allWeekDaysArray: allWeekDaysArray,
|
||||||
|
allMonthsArray: allMonthsArray,
|
||||||
allLongDateFormat: allLongDateFormat,
|
allLongDateFormat: allLongDateFormat,
|
||||||
allLongDateFormatArray: allLongDateFormatArray,
|
allLongDateFormatArray: allLongDateFormatArray,
|
||||||
allShortDateFormat: allShortDateFormat,
|
allShortDateFormat: allShortDateFormat,
|
||||||
|
|||||||
+4
-1
@@ -46,8 +46,9 @@ import 'vuetify/styles';
|
|||||||
|
|
||||||
import * as echarts from 'echarts/core';
|
import * as echarts from 'echarts/core';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
import { PieChart } from 'echarts/charts';
|
import { BarChart, PieChart } from 'echarts/charts';
|
||||||
import {
|
import {
|
||||||
|
GridComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
@@ -363,7 +364,9 @@ const vuetify = createVuetify({
|
|||||||
|
|
||||||
echarts.use([
|
echarts.use([
|
||||||
CanvasRenderer,
|
CanvasRenderer,
|
||||||
|
BarChart,
|
||||||
PieChart,
|
PieChart,
|
||||||
|
GridComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
LegendComponent
|
LegendComponent
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -133,6 +133,11 @@ export function getDayOfWeekName(date) {
|
|||||||
return dateTimeConstants.allWeekDaysArray[dayOfWeek].name;
|
return dateTimeConstants.allWeekDaysArray[dayOfWeek].name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMonthName(date) {
|
||||||
|
const dayOfWeek = moment(date).month();
|
||||||
|
return dateTimeConstants.allMonthsArray[dayOfWeek];
|
||||||
|
}
|
||||||
|
|
||||||
export function getHour(date) {
|
export function getHour(date) {
|
||||||
return moment(date).hour();
|
return moment(date).hour();
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-1
@@ -258,7 +258,7 @@ export default {
|
|||||||
|
|
||||||
return axios.get('v1/transactions/statistics.json' + (queryParams.length ? '?' + queryParams.join('&') : ''));
|
return axios.get('v1/transactions/statistics.json' + (queryParams.length ? '?' + queryParams.join('&') : ''));
|
||||||
},
|
},
|
||||||
getTransactionAmounts: ({ today, thisWeek, thisMonth, thisYear }) => {
|
getTransactionAmounts: ({ today, thisWeek, thisMonth, thisYear, lastMonth, monthBeforeLastMonth, monthBeforeLast2Months, monthBeforeLast3Months, monthBeforeLast4Months }) => {
|
||||||
const queryParams = [];
|
const queryParams = [];
|
||||||
|
|
||||||
if (today) {
|
if (today) {
|
||||||
@@ -277,6 +277,26 @@ export default {
|
|||||||
queryParams.push(`thisYear_${thisYear.startTime}_${thisYear.endTime}`);
|
queryParams.push(`thisYear_${thisYear.startTime}_${thisYear.endTime}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastMonth) {
|
||||||
|
queryParams.push(`lastMonth_${lastMonth.startTime}_${lastMonth.endTime}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monthBeforeLastMonth) {
|
||||||
|
queryParams.push(`monthBeforeLastMonth_${monthBeforeLastMonth.startTime}_${monthBeforeLastMonth.endTime}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monthBeforeLast2Months) {
|
||||||
|
queryParams.push(`monthBeforeLast2Months_${monthBeforeLast2Months.startTime}_${monthBeforeLast2Months.endTime}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monthBeforeLast3Months) {
|
||||||
|
queryParams.push(`monthBeforeLast3Months_${monthBeforeLast3Months.startTime}_${monthBeforeLast3Months.endTime}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monthBeforeLast4Months) {
|
||||||
|
queryParams.push(`monthBeforeLast4Months_${monthBeforeLast4Months.startTime}_${monthBeforeLast4Months.endTime}`);
|
||||||
|
}
|
||||||
|
|
||||||
return axios.get('v1/transactions/amounts.json' + (queryParams.length ? '?query=' + queryParams.join('|') : ''));
|
return axios.get('v1/transactions/amounts.json' + (queryParams.length ? '?query=' + queryParams.join('|') : ''));
|
||||||
},
|
},
|
||||||
getTransaction: ({ id }) => {
|
getTransaction: ({ id }) => {
|
||||||
|
|||||||
@@ -837,6 +837,7 @@ export default {
|
|||||||
'PIN code is wrong': 'PIN code is wrong',
|
'PIN code is wrong': 'PIN code is wrong',
|
||||||
'Sign Up': 'Sign Up',
|
'Sign Up': 'Sign Up',
|
||||||
'Overview': 'Overview',
|
'Overview': 'Overview',
|
||||||
|
'Trend in Income and Expense': 'Trend in Income and Expense',
|
||||||
'View Details': 'View Details',
|
'View Details': 'View Details',
|
||||||
'Transaction List': 'Transaction List',
|
'Transaction List': 'Transaction List',
|
||||||
'Account List': 'Account List',
|
'Account List': 'Account List',
|
||||||
|
|||||||
@@ -837,6 +837,7 @@ export default {
|
|||||||
'PIN code is wrong': 'PIN码错误',
|
'PIN code is wrong': 'PIN码错误',
|
||||||
'Sign Up': '注册',
|
'Sign Up': '注册',
|
||||||
'Overview': '总览',
|
'Overview': '总览',
|
||||||
|
'Trend in Income and Expense': '收入与支出趋势',
|
||||||
'View Details': '查看详情',
|
'View Details': '查看详情',
|
||||||
'Transaction List': '交易列表',
|
'Transaction List': '交易列表',
|
||||||
'Account List': '账户列表',
|
'Account List': '账户列表',
|
||||||
|
|||||||
+65
-9
@@ -5,6 +5,7 @@ import { useExchangeRatesStore } from './exchangeRates.js';
|
|||||||
|
|
||||||
import { isNumber, isEquals } from '@/lib/common.js';
|
import { isNumber, isEquals } from '@/lib/common.js';
|
||||||
import {
|
import {
|
||||||
|
getUnixTimeBeforeUnixTime,
|
||||||
getTodayFirstUnixTime,
|
getTodayFirstUnixTime,
|
||||||
getTodayLastUnixTime,
|
getTodayLastUnixTime,
|
||||||
getThisWeekFirstUnixTime,
|
getThisWeekFirstUnixTime,
|
||||||
@@ -31,6 +32,21 @@ function updateTransactionDateRange(state) {
|
|||||||
|
|
||||||
state.transactionDataRange.thisYear.startTime = getThisYearFirstUnixTime();
|
state.transactionDataRange.thisYear.startTime = getThisYearFirstUnixTime();
|
||||||
state.transactionDataRange.thisYear.endTime = getThisYearLastUnixTime();
|
state.transactionDataRange.thisYear.endTime = getThisYearLastUnixTime();
|
||||||
|
|
||||||
|
state.transactionDataRange.lastMonth.startTime = getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 1, 'months');
|
||||||
|
state.transactionDataRange.lastMonth.endTime = getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 1, 'months');
|
||||||
|
|
||||||
|
state.transactionDataRange.monthBeforeLastMonth.startTime = getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 2, 'months');
|
||||||
|
state.transactionDataRange.monthBeforeLastMonth.endTime = getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 2, 'months');
|
||||||
|
|
||||||
|
state.transactionDataRange.monthBeforeLast2Months.startTime = getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 3, 'months');
|
||||||
|
state.transactionDataRange.monthBeforeLast2Months.endTime = getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 3, 'months');
|
||||||
|
|
||||||
|
state.transactionDataRange.monthBeforeLast3Months.startTime = getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 4, 'months');
|
||||||
|
state.transactionDataRange.monthBeforeLast3Months.endTime = getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 4, 'months');
|
||||||
|
|
||||||
|
state.transactionDataRange.monthBeforeLast4Months.startTime = getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 5, 'months');
|
||||||
|
state.transactionDataRange.monthBeforeLast4Months.endTime = getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 5, 'months');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useOverviewStore = defineStore('overview', {
|
export const useOverviewStore = defineStore('overview', {
|
||||||
@@ -51,8 +67,31 @@ export const useOverviewStore = defineStore('overview', {
|
|||||||
thisYear: {
|
thisYear: {
|
||||||
startTime: getThisYearFirstUnixTime(),
|
startTime: getThisYearFirstUnixTime(),
|
||||||
endTime: getThisYearLastUnixTime()
|
endTime: getThisYearLastUnixTime()
|
||||||
|
},
|
||||||
|
lastMonth: {
|
||||||
|
startTime: getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 1, 'months'),
|
||||||
|
endTime: getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 1, 'months')
|
||||||
|
},
|
||||||
|
monthBeforeLastMonth: {
|
||||||
|
startTime: getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 2, 'months'),
|
||||||
|
endTime: getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 2, 'months')
|
||||||
|
},
|
||||||
|
monthBeforeLast2Months: {
|
||||||
|
startTime: getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 3, 'months'),
|
||||||
|
endTime: getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 3, 'months')
|
||||||
|
},
|
||||||
|
monthBeforeLast3Months: {
|
||||||
|
startTime: getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 4, 'months'),
|
||||||
|
endTime: getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 4, 'months')
|
||||||
|
},
|
||||||
|
monthBeforeLast4Months: {
|
||||||
|
startTime: getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 5, 'months'),
|
||||||
|
endTime: getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 5, 'months')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
transactionOverviewOptions: {
|
||||||
|
loadLast5Months: false
|
||||||
|
},
|
||||||
transactionOverviewData: {},
|
transactionOverviewData: {},
|
||||||
transactionOverviewStateInvalid: true
|
transactionOverviewStateInvalid: true
|
||||||
}),
|
}),
|
||||||
@@ -78,7 +117,7 @@ export const useOverviewStore = defineStore('overview', {
|
|||||||
const finalOverviewData = {};
|
const finalOverviewData = {};
|
||||||
const defaultCurrency = userStore.currentUserDefaultCurrency;
|
const defaultCurrency = userStore.currentUserDefaultCurrency;
|
||||||
|
|
||||||
[ 'today', 'thisWeek', 'thisMonth', 'thisYear' ].forEach(field => {
|
[ 'today', 'thisWeek', 'thisMonth', 'thisYear', 'lastMonth', 'monthBeforeLastMonth', 'monthBeforeLast2Months', 'monthBeforeLast3Months', 'monthBeforeLast4Months' ].forEach(field => {
|
||||||
if (!Object.prototype.hasOwnProperty.call(overviewData, field)) {
|
if (!Object.prototype.hasOwnProperty.call(overviewData, field)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -139,31 +178,47 @@ export const useOverviewStore = defineStore('overview', {
|
|||||||
},
|
},
|
||||||
resetTransactionOverview() {
|
resetTransactionOverview() {
|
||||||
updateTransactionDateRange(this);
|
updateTransactionDateRange(this);
|
||||||
|
this.transactionOverviewOptions.loadLast5Months = false;
|
||||||
this.transactionOverviewData = {};
|
this.transactionOverviewData = {};
|
||||||
this.transactionOverviewStateInvalid = true;
|
this.transactionOverviewStateInvalid = true;
|
||||||
},
|
},
|
||||||
loadTransactionOverview({ force }) {
|
loadTransactionOverview({ force, loadLast5Months }) {
|
||||||
const self = this;
|
const self = this;
|
||||||
let dateChanged = false;
|
let dateChanged = false;
|
||||||
|
let rangeChanged = false;
|
||||||
|
|
||||||
if (self.transactionDataRange.today.startTime !== getTodayFirstUnixTime()) {
|
if (self.transactionDataRange.today.startTime !== getTodayFirstUnixTime()) {
|
||||||
dateChanged = true;
|
dateChanged = true;
|
||||||
updateTransactionDateRange(self);
|
updateTransactionDateRange(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dateChanged && !force && !self.transactionOverviewStateInvalid) {
|
if (loadLast5Months && !self.transactionOverviewOptions.loadLast5Months) {
|
||||||
|
rangeChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dateChanged && !rangeChanged && !force && !self.transactionOverviewStateInvalid) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
resolve(self.transactionOverviewData);
|
resolve(self.transactionOverviewData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestParams = {
|
||||||
|
today: self.transactionDataRange.today,
|
||||||
|
thisWeek: self.transactionDataRange.thisWeek,
|
||||||
|
thisMonth: self.transactionDataRange.thisMonth,
|
||||||
|
thisYear: self.transactionDataRange.thisYear
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loadLast5Months) {
|
||||||
|
requestParams.lastMonth = self.transactionDataRange.lastMonth;
|
||||||
|
requestParams.monthBeforeLastMonth = self.transactionDataRange.monthBeforeLastMonth;
|
||||||
|
requestParams.monthBeforeLast2Months = self.transactionDataRange.monthBeforeLast2Months;
|
||||||
|
requestParams.monthBeforeLast3Months = self.transactionDataRange.monthBeforeLast3Months;
|
||||||
|
requestParams.monthBeforeLast4Months = self.transactionDataRange.monthBeforeLast4Months;
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
services.getTransactionAmounts({
|
services.getTransactionAmounts(requestParams).then(response => {
|
||||||
today: self.transactionDataRange.today,
|
|
||||||
thisWeek: self.transactionDataRange.thisWeek,
|
|
||||||
thisMonth: self.transactionDataRange.thisMonth,
|
|
||||||
thisYear: self.transactionDataRange.thisYear
|
|
||||||
}).then(response => {
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
if (!data || !data.success || !data.result) {
|
if (!data || !data.success || !data.result) {
|
||||||
@@ -181,6 +236,7 @@ export const useOverviewStore = defineStore('overview', {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.transactionOverviewData = data.result;
|
self.transactionOverviewData = data.result;
|
||||||
|
self.transactionOverviewOptions.loadLast5Months = loadLast5Months;
|
||||||
|
|
||||||
resolve(data.result);
|
resolve(data.result);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
<v-card :class="{ 'disabled': loadingOverview }">
|
<v-card :class="{ 'disabled': loadingOverview }">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
<span class="text-2xl font-weight-bold">{{ displayDateRange.thisMonth.displayTime }}</span>
|
<div class="d-flex align-baseline">
|
||||||
<span>·</span>
|
<span class="text-2xl font-weight-bold">{{ displayDateRange.thisMonth.displayTime }}</span>
|
||||||
<small>{{ $t('Expense') }}</small>
|
<span>·</span>
|
||||||
|
<small>{{ $t('Expense') }}</small>
|
||||||
|
</div>
|
||||||
<v-btn density="compact" color="default" variant="text"
|
<v-btn density="compact" color="default" variant="text"
|
||||||
class="ml-2" :icon="true"
|
class="ml-2" :icon="true"
|
||||||
v-if="!loadingOverview" @click="reload(true)">
|
v-if="!loadingOverview" @click="reload(true)">
|
||||||
@@ -25,11 +27,13 @@
|
|||||||
<v-icon :icon="showAmountInHomePage ? icons.eyeSlash : icons.eye" size="20" />
|
<v-icon :icon="showAmountInHomePage ? icons.eyeSlash : icons.eye" size="20" />
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</h5>
|
</h5>
|
||||||
<p>
|
<div class="mt-2 mb-3">
|
||||||
<span class="mr-2">{{ $t('Monthly income') }}</span>
|
<span class="mr-2">{{ $t('Monthly income') }}</span>
|
||||||
<span>{{ transactionOverview && transactionOverview.thisMonth ? getDisplayIncomeAmount(transactionOverview.thisMonth) : '-' }}</span>
|
<span>{{ transactionOverview && transactionOverview.thisMonth ? getDisplayIncomeAmount(transactionOverview.thisMonth) : '-' }}</span>
|
||||||
</p>
|
</div>
|
||||||
<v-btn size="small" to="/transactions">{{ $t('View Details') }}</v-btn>
|
<v-btn size="small" to="/transactions?dateType=7">{{ $t('View Details') }}</v-btn>
|
||||||
|
<v-img class="overview-card-background" src="img/desktop/card-background.png"/>
|
||||||
|
<v-img class="overview-card-background-image" width="116px" src="img/desktop/document.svg"/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -97,13 +101,21 @@
|
|||||||
</template>
|
</template>
|
||||||
</income-expense-overview-card>
|
</income-expense-overview-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<monthly-income-and-expense-card :data="monthlyIncomeAndExpenseData"
|
||||||
|
:disabled="loadingOverview" :is-dark-mode="isDarkMode" />
|
||||||
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<snack-bar ref="snackbar" />
|
<snack-bar ref="snackbar" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { useTheme } from 'vuetify';
|
||||||
|
|
||||||
import IncomeExpenseOverviewCard from './overview/IncomeExpenseOverviewCard.vue';
|
import IncomeExpenseOverviewCard from './overview/IncomeExpenseOverviewCard.vue';
|
||||||
|
import MonthlyIncomeAndExpenseCard from './overview/MonthlyIncomeAndExpenseCard.vue';
|
||||||
|
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import { useSettingsStore } from '@/stores/setting.js';
|
import { useSettingsStore } from '@/stores/setting.js';
|
||||||
@@ -117,6 +129,7 @@ import {
|
|||||||
mdiRefresh,
|
mdiRefresh,
|
||||||
mdiEyeOutline,
|
mdiEyeOutline,
|
||||||
mdiEyeOffOutline,
|
mdiEyeOffOutline,
|
||||||
|
mdiCartOutline,
|
||||||
mdiCalendarTodayOutline,
|
mdiCalendarTodayOutline,
|
||||||
mdiCalendarWeekOutline,
|
mdiCalendarWeekOutline,
|
||||||
mdiCalendarMonthOutline,
|
mdiCalendarMonthOutline,
|
||||||
@@ -127,7 +140,8 @@ import {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
IncomeExpenseOverviewCard
|
IncomeExpenseOverviewCard,
|
||||||
|
MonthlyIncomeAndExpenseCard
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -136,6 +150,7 @@ export default {
|
|||||||
refresh: mdiRefresh,
|
refresh: mdiRefresh,
|
||||||
eye: mdiEyeOutline,
|
eye: mdiEyeOutline,
|
||||||
eyeSlash: mdiEyeOffOutline,
|
eyeSlash: mdiEyeOffOutline,
|
||||||
|
cart: mdiCartOutline,
|
||||||
calendarToday: mdiCalendarTodayOutline,
|
calendarToday: mdiCalendarTodayOutline,
|
||||||
calendarWeek: mdiCalendarWeekOutline,
|
calendarWeek: mdiCalendarWeekOutline,
|
||||||
calendarMonth: mdiCalendarMonthOutline,
|
calendarMonth: mdiCalendarMonthOutline,
|
||||||
@@ -147,6 +162,9 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useSettingsStore, useUserStore, useOverviewStore),
|
...mapStores(useSettingsStore, useUserStore, useOverviewStore),
|
||||||
|
isDarkMode() {
|
||||||
|
return this.globalTheme.global.name.value === 'dark';
|
||||||
|
},
|
||||||
showAmountInHomePage: {
|
showAmountInHomePage: {
|
||||||
get: function() {
|
get: function() {
|
||||||
return this.settingsStore.appSettings.showAmountInHomePage;
|
return this.settingsStore.appSettings.showAmountInHomePage;
|
||||||
@@ -184,6 +202,34 @@ export default {
|
|||||||
},
|
},
|
||||||
transactionOverview() {
|
transactionOverview() {
|
||||||
return this.overviewStore.transactionOverview;
|
return this.overviewStore.transactionOverview;
|
||||||
|
},
|
||||||
|
monthlyIncomeAndExpenseData() {
|
||||||
|
const self = this;
|
||||||
|
const data = [];
|
||||||
|
|
||||||
|
[ 'monthBeforeLast4Months', 'monthBeforeLast3Months', 'monthBeforeLast2Months', 'monthBeforeLastMonth', 'lastMonth', 'thisMonth' ].forEach(fieldName => {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(self.transactionOverview, fieldName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateRange = self.overviewStore.transactionDataRange[fieldName];
|
||||||
|
|
||||||
|
if (!dateRange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = self.overviewStore.transactionOverview[fieldName];
|
||||||
|
|
||||||
|
data.push({
|
||||||
|
monthStartTime: dateRange.startTime,
|
||||||
|
incomeAmount: item ? item.incomeAmount : 0,
|
||||||
|
expenseAmount: item ? item.expenseAmount : 0,
|
||||||
|
incompleteIncomeAmount: item ? item.incompleteIncomeAmount : true,
|
||||||
|
incompleteExpenseAmount: item ? item.incompleteExpenseAmount : true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -191,6 +237,13 @@ export default {
|
|||||||
this.reload(false);
|
this.reload(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return {
|
||||||
|
globalTheme: theme
|
||||||
|
};
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
reload(force) {
|
reload(force) {
|
||||||
const self = this;
|
const self = this;
|
||||||
@@ -198,7 +251,8 @@ export default {
|
|||||||
self.loadingOverview = true;
|
self.loadingOverview = true;
|
||||||
|
|
||||||
self.overviewStore.loadTransactionOverview({
|
self.overviewStore.loadTransactionOverview({
|
||||||
force: force
|
force: force,
|
||||||
|
loadLast5Months: true
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
self.loadingOverview = false;
|
self.loadingOverview = false;
|
||||||
|
|
||||||
@@ -235,3 +289,19 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.overview-card-background {
|
||||||
|
position: absolute;
|
||||||
|
inline-size: 9rem;
|
||||||
|
inset-block-end: 0;
|
||||||
|
inset-inline-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card-background-image {
|
||||||
|
position: absolute;
|
||||||
|
inline-size: 5rem;
|
||||||
|
inset-block-end: 0.5rem;
|
||||||
|
inset-inline-end: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
<template>
|
||||||
|
<v-card :class="{ 'disabled': disabled }">
|
||||||
|
<template #title>
|
||||||
|
<span class="text-2xl font-weight-bold">{{ $t('Trend in Income and Expense') }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-chart class="overview-monthly-chart" autoresize :option="chartOptions" />
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapStores } from 'pinia';
|
||||||
|
import { useSettingsStore } from '@/stores/setting.js';
|
||||||
|
import { useUserStore } from '@/stores/user.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseDateFromUnixTime,
|
||||||
|
getMonthName
|
||||||
|
} from '@/lib/datetime.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
'data',
|
||||||
|
'disabled',
|
||||||
|
'isDarkMode'
|
||||||
|
],
|
||||||
|
computed: {
|
||||||
|
...mapStores(useSettingsStore, useUserStore),
|
||||||
|
showAmountInHomePage() {
|
||||||
|
return this.settingsStore.appSettings.showAmountInHomePage;
|
||||||
|
},
|
||||||
|
defaultCurrency() {
|
||||||
|
return this.userStore.currentUserDefaultCurrency;
|
||||||
|
},
|
||||||
|
chartOptions() {
|
||||||
|
const self = this;
|
||||||
|
const monthNames = [];
|
||||||
|
const incomeAmounts = [];
|
||||||
|
const expenseAmounts = [];
|
||||||
|
let minAmount = 0;
|
||||||
|
let maxAmount = 0;
|
||||||
|
|
||||||
|
if (self.data) {
|
||||||
|
for (let i = 0; i < self.data.length; i++) {
|
||||||
|
const item = self.data[i];
|
||||||
|
const month = getMonthName(parseDateFromUnixTime(item.monthStartTime));
|
||||||
|
|
||||||
|
monthNames.push(self.$locale.getMonthShortName(month));
|
||||||
|
incomeAmounts.push(item.incomeAmount);
|
||||||
|
expenseAmounts.push(-item.expenseAmount);
|
||||||
|
|
||||||
|
if (item.incomeAmount > maxAmount) {
|
||||||
|
maxAmount = item.incomeAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-item.expenseAmount > maxAmount) {
|
||||||
|
maxAmount = -item.expenseAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.incomeAmount < minAmount) {
|
||||||
|
minAmount = item.incomeAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-item.expenseAmount < minAmount) {
|
||||||
|
minAmount = -item.expenseAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let amountGap = maxAmount - minAmount;
|
||||||
|
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
},
|
||||||
|
backgroundColor: self.isDarkMode ? '#333' : '#fff',
|
||||||
|
borderColor: self.isDarkMode ? '#333' : '#fff',
|
||||||
|
textStyle: {
|
||||||
|
color: self.isDarkMode ? '#eee' : '#333'
|
||||||
|
},
|
||||||
|
formatter: params => {
|
||||||
|
let incomeAmount = 0;
|
||||||
|
let incomeColor = '#cc4a66';
|
||||||
|
let expenseAmount = 0;
|
||||||
|
let expenseColor = '#4dd291';
|
||||||
|
|
||||||
|
for (let i = 0; i < params.length; i++) {
|
||||||
|
const param = params[i];
|
||||||
|
const dataIndex = param.dataIndex;
|
||||||
|
const data = self.data[dataIndex];
|
||||||
|
|
||||||
|
if (param.seriesId === 'seriesIncome') {
|
||||||
|
incomeAmount = self.getDisplayIncomeAmount(data);
|
||||||
|
incomeColor = param.color;
|
||||||
|
} else if (param.seriesId === 'seriesExpense') {
|
||||||
|
expenseAmount = self.getDisplayExpenseAmount(data);
|
||||||
|
expenseColor = param.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<table>` +
|
||||||
|
`<thead>` +
|
||||||
|
`<tr>` +
|
||||||
|
`<td colspan="2" class="text-left">${params[0].name}</td>` +
|
||||||
|
`</tr>` +
|
||||||
|
`</thead>` +
|
||||||
|
`<tbody>` +
|
||||||
|
`<tr>` +
|
||||||
|
`<td><span class="overview-monthly-chart-tooltip-indicator mr-1" style="background-color: ${incomeColor}"></span><span class="mr-4">${self.$t('Income')}</span></td>` +
|
||||||
|
`<td><strong>${incomeAmount}</strong></td>` +
|
||||||
|
`</tr>` +
|
||||||
|
`<tr>` +
|
||||||
|
`<td><span class="overview-monthly-chart-tooltip-indicator mr-1" style="background-color: ${expenseColor}"></span><span class="mr-4">${self.$t('Expense')}</span></td>` +
|
||||||
|
`<td><strong>${expenseAmount}</strong></td>` +
|
||||||
|
`</tr>` +
|
||||||
|
`</tbody>` +
|
||||||
|
`</table>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
bottom: 20,
|
||||||
|
itemWidth: 14,
|
||||||
|
itemHeight: 14,
|
||||||
|
textStyle: {
|
||||||
|
color: self.isDarkMode ? '#eee' : '#333'
|
||||||
|
},
|
||||||
|
icon: 'circle',
|
||||||
|
data: [ self.$t('Income'), self.$t('Expense') ]
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '20px',
|
||||||
|
right: '20px',
|
||||||
|
top: '10px',
|
||||||
|
bottom: '100px'
|
||||||
|
},
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
data: monthNames,
|
||||||
|
axisLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
padding: [ 20, 0, 0, 0 ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
min: minAmount - amountGap / 20,
|
||||||
|
max: maxAmount,
|
||||||
|
splitNumber: 10,
|
||||||
|
axisLabel: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
min: minAmount,
|
||||||
|
max: maxAmount + amountGap / 20,
|
||||||
|
splitNumber: 10,
|
||||||
|
axisLabel: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'bar',
|
||||||
|
id: 'seriesIncome',
|
||||||
|
name: self.$t('Income'),
|
||||||
|
yAxisIndex: 0,
|
||||||
|
stack: 'Total',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#cc4a66',
|
||||||
|
borderRadius: 16
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series',
|
||||||
|
labelLine: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
barMaxWidth: 40,
|
||||||
|
data: incomeAmounts
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'bar',
|
||||||
|
id: 'seriesExpense',
|
||||||
|
name: self.$t('Expense'),
|
||||||
|
yAxisIndex: 1,
|
||||||
|
stack: 'Total',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#4dd291',
|
||||||
|
borderRadius: 16
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series',
|
||||||
|
labelLine: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
barMaxWidth: 40,
|
||||||
|
data: expenseAmounts
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getDisplayCurrency(value, currencyCode) {
|
||||||
|
return this.$locale.getDisplayCurrency(value, currencyCode, {
|
||||||
|
currencyDisplayMode: this.settingsStore.appSettings.currencyDisplayMode,
|
||||||
|
enableThousandsSeparator: this.settingsStore.appSettings.thousandsSeparator
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getDisplayAmount(amount, incomplete) {
|
||||||
|
if (!this.showAmountInHomePage) {
|
||||||
|
return this.getDisplayCurrency('***', this.defaultCurrency);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getDisplayCurrency(amount, this.defaultCurrency) + (incomplete ? '+' : '');
|
||||||
|
},
|
||||||
|
getDisplayIncomeAmount(category) {
|
||||||
|
return this.getDisplayAmount(category.incomeAmount, category.incompleteIncomeAmount);
|
||||||
|
},
|
||||||
|
getDisplayExpenseAmount(category) {
|
||||||
|
return this.getDisplayAmount(category.expenseAmount, category.incompleteExpenseAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.overview-monthly-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-monthly-chart-tooltip-indicator {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -270,6 +270,11 @@
|
|||||||
"url": "https://materialdesignicons.com",
|
"url": "https://materialdesignicons.com",
|
||||||
"licenseUrl": "https://github.com/Templarian/MaterialDesign-JS/blob/v7.2.96/LICENSE"
|
"licenseUrl": "https://github.com/Templarian/MaterialDesign-JS/blob/v7.2.96/LICENSE"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Solar Icons Set",
|
||||||
|
"url": "https://www.figma.com/community/file/1166831539721848736/Solar-Icons-Set",
|
||||||
|
"licenseUrl": "https://creativecommons.org/licenses/by/4.0"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Hand drawn minimal background",
|
"name": "Hand drawn minimal background",
|
||||||
"url": "https://www.freepik.com/free-vector/hand-drawn-minimal-background_15441932.htm",
|
"url": "https://www.freepik.com/free-vector/hand-drawn-minimal-background_15441932.htm",
|
||||||
|
|||||||
Reference in New Issue
Block a user