mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-10-08 05:43:01 +00:00
Various code for currency exchange rate support
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
<script>
|
||||
import {defineComponent} from 'vue';
|
||||
import Preferences from "./api/preferences";
|
||||
import Prefs from "./api/v2/preferences";
|
||||
import Currencies from "./api/currencies";
|
||||
import {useFireflyIIIStore} from 'stores/fireflyiii'
|
||||
|
||||
@@ -74,10 +75,22 @@ export default defineComponent(
|
||||
});
|
||||
};
|
||||
|
||||
const getLocale = function () {
|
||||
return (new Prefs).get('locale').then(data => {
|
||||
const locale = data.data.data.attributes.data.replace('_','-');
|
||||
|
||||
ffStore.setLocale(locale);
|
||||
}).catch((err) => {
|
||||
console.error('Could not load locale.')
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
getDefaultCurrency().then(() => {
|
||||
getViewRange();
|
||||
getListPageSize();
|
||||
getLocale();
|
||||
});
|
||||
}
|
||||
})
|
||||
|
38
frontend/src/api/v2/budgets/sum.js
vendored
Normal file
38
frontend/src/api/v2/budgets/sum.js
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* list.js
|
||||
* Copyright (c) 2022 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {api} from "boot/axios";
|
||||
import {format} from "date-fns";
|
||||
|
||||
export default class Sum {
|
||||
budgeted(start, end) {
|
||||
let url = 'api/v2/budgets/sum/budgeted';
|
||||
let startStr = format(start, 'y-MM-dd');
|
||||
let endStr = format(end, 'y-MM-dd');
|
||||
return api.get(url, {params: {start: startStr, end: endStr}});
|
||||
}
|
||||
|
||||
// /*paid(start, end) {
|
||||
// let url = 'api/v2/bills/sum/paid';
|
||||
// let startStr = format(start, 'y-MM-dd');
|
||||
// let endStr = format(end, 'y-MM-dd');
|
||||
// return api.get(url, {params: {start: startStr, end: endStr}});
|
||||
// }*/
|
||||
}
|
30
frontend/src/api/v2/preferences/index.js
vendored
Normal file
30
frontend/src/api/v2/preferences/index.js
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* basic.js
|
||||
* Copyright (c) 2021 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {api} from "boot/axios";
|
||||
|
||||
export default class Preferences {
|
||||
get(name) {
|
||||
return api.get('/api/v2/preferences/' + name);
|
||||
}
|
||||
// postByName(name, value) {
|
||||
// return api.post('/api/v1/preferences', {name: name, data: value});
|
||||
// }
|
||||
}
|
@@ -26,7 +26,7 @@
|
||||
<q-card bordered>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ $t('firefly.bills_to_pay') }}</q-item-label>
|
||||
<q-item-label><strong>{{ $t('firefly.bills_to_pay') }}</strong></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator/>
|
||||
@@ -41,36 +41,22 @@
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-separator vertical/>
|
||||
<q-card-section>
|
||||
{{ $t('firefly.bills_to_pay') }}:
|
||||
<q-card-section v-if="0 === unpaid.length && 0 === paid.length">
|
||||
You have no bills
|
||||
</q-card-section>
|
||||
<q-card-section v-if="unpaid.length > 0 || paid.length > 0">
|
||||
<span :title="formatAmount(this.currency, this.unpaidAmount)">{{ $t('firefly.bills_to_pay') }}</span>:
|
||||
<span v-for="(bill, index) in unpaid">
|
||||
{{ formatAmount(bill.code, bill.sum) }}
|
||||
<span v-if="index+1 !== unpaid.length">, </span>
|
||||
</span>
|
||||
<span v-if="bill.native">(</span>{{ formatAmount(bill.code, bill.sum) }}<span
|
||||
v-if="bill.native">)</span><span v-if="index+1 !== unpaid.length">, </span></span>
|
||||
<br/>
|
||||
{{ $t('firefly.bills_paid') }}:
|
||||
<span v-for="(bill, index) in paid">
|
||||
{{ formatAmount(bill.code, bill.sum) }}
|
||||
<span v-if="index+1 !== paid.length">, </span>
|
||||
</span>
|
||||
<span :title="formatAmount(this.currency, this.paidAmount)">{{ $t('firefly.bills_paid') }}</span>:
|
||||
<span v-for="(bill, index) in paid"><span v-if="bill.native">(</span>{{
|
||||
formatAmount(bill.code, bill.sum)
|
||||
}}<span v-if="bill.native">)</span><span
|
||||
v-if="index+1 !== paid.length">, </span></span>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
<!--
|
||||
<q-card-section class="q-pt-xs">
|
||||
<div class="text-overline">
|
||||
|
||||
<span class="float-right">
|
||||
<span class="text-grey-4 fas fa-redo-alt" style="cursor: pointer;" @click="triggerForcedUpgrade"></span>
|
||||
</span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pt-xs">
|
||||
<span v-for="(bill, index) in unpaid">
|
||||
{{ formatAmount(bill.code, bill.sum) }}
|
||||
<span v-if="index+1 !== unpaid.length">, </span>
|
||||
</span>
|
||||
</q-card-section>
|
||||
-->
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -85,7 +71,7 @@ export default {
|
||||
store: null,
|
||||
unpaid: [],
|
||||
paid: [],
|
||||
//percentage: 0,
|
||||
currency: 'EUR',
|
||||
unpaidAmount: 0.0,
|
||||
paidAmount: 0.0,
|
||||
range: {
|
||||
@@ -100,16 +86,18 @@ export default {
|
||||
if (0 === this.unpaidAmount) {
|
||||
return 100;
|
||||
}
|
||||
const sum = this.unpaidAmount + this.paidAmount;
|
||||
if (0.0 === this.paidAmount) {
|
||||
return 0;
|
||||
}
|
||||
return (this.paidAmount / sum) * 100;
|
||||
const pct = (this.paidAmount / this.unpaidAmount) * 100;
|
||||
if (pct > 100) {
|
||||
return 100;
|
||||
}
|
||||
return pct;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.store = useFireflyIIIStore();
|
||||
|
||||
// TODO this code snippet is recycled a lot.
|
||||
if (null === this.range.start || null === this.range.end) {
|
||||
// subscribe, then update:
|
||||
@@ -133,25 +121,30 @@ export default {
|
||||
const start = new Date(this.store.getRange.start);
|
||||
const end = new Date(this.store.getRange.end);
|
||||
const sum = new Sum;
|
||||
this.currency = this.store.getCurrencyCode;
|
||||
sum.unpaid(start, end).then((response) => this.parseUnpaidResponse(response.data));
|
||||
sum.paid(start, end).then((response) => this.parsePaidResponse(response.data));
|
||||
}
|
||||
},
|
||||
// TODO this method is recycled a lot.
|
||||
formatAmount: function (currencyCode, amount) {
|
||||
// TODO not yet internationalized
|
||||
return Intl.NumberFormat('en-US', {style: 'currency', currency: currencyCode}).format(amount);
|
||||
return Intl.NumberFormat(this.store.getLocale, {style: 'currency', currency: currencyCode}).format(amount);
|
||||
},
|
||||
parseUnpaidResponse: function (data) {
|
||||
for (let i in data) {
|
||||
if (data.hasOwnProperty(i)) {
|
||||
const current = data[i];
|
||||
const hasNative = current.native_id !== current.id && current.native_sum !== '0';
|
||||
this.unpaid.push(
|
||||
{
|
||||
sum: current.sum,
|
||||
code: current.code,
|
||||
native: hasNative,
|
||||
}
|
||||
);
|
||||
this.unpaidAmount = this.unpaidAmount + parseFloat(current.sum);
|
||||
if (hasNative || current.native_id === current.id) {
|
||||
this.unpaidAmount = this.unpaidAmount + parseFloat(current.native_sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -159,20 +152,20 @@ export default {
|
||||
for (let i in data) {
|
||||
if (data.hasOwnProperty(i)) {
|
||||
const current = data[i];
|
||||
const hasNative = current.native_id !== current.id && parseFloat(current.native_sum) !== 0.0;
|
||||
this.paid.push(
|
||||
{
|
||||
sum: current.sum,
|
||||
code: current.code,
|
||||
native: hasNative,
|
||||
}
|
||||
);
|
||||
this.paidAmount = this.paidAmount + (parseFloat(current.sum) * -1);
|
||||
if (hasNative || current.native_id === current.id) {
|
||||
this.paidAmount = this.paidAmount + (parseFloat(current.native_sum) * -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
152
frontend/src/components/dashboard/SpendInsightBox.vue
Normal file
152
frontend/src/components/dashboard/SpendInsightBox.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<!--
|
||||
- BillInsightBox.vue
|
||||
- Copyright (c) 2022 james@firefly-iii.org
|
||||
-
|
||||
- This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<!-- TODO most left? q-mr-sm -->
|
||||
<!-- TODO middle? dan q-mx-sm -->
|
||||
<!-- TODO right? dan q-ml-sm -->
|
||||
<div class="q-mx-sm">
|
||||
<q-card bordered>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label><strong>Spend</strong></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator/>
|
||||
<q-card-section horizontal>
|
||||
<q-card-section>
|
||||
<q-circular-progress
|
||||
:value="percentage"
|
||||
size="50px"
|
||||
:thickness="0.22"
|
||||
color="green"
|
||||
track-color="grey-3"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-separator vertical/>
|
||||
<q-card-section v-if="0 === budgeted.length && 0 === spent.length">
|
||||
You have no budgets set
|
||||
</q-card-section>
|
||||
<q-card-section v-if="budgeted.length > 0 || spent.length > 0">
|
||||
<span :title="formatAmount(this.currency, this.budgetedAmount)">Budgeted</span>:
|
||||
<span v-for="(budget, index) in budgeted"><span v-if="budget.native">(</span>{{ formatAmount(budget.code, budget.sum) }}<span v-if="budget.native">)</span><span
|
||||
v-if="index+1 !== budgeted.length">, </span></span>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useFireflyIIIStore} from "../../stores/fireflyiii";
|
||||
import Sum from "../../api/v2/budgets/sum";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
store: null,
|
||||
budgeted: [],
|
||||
spent: [],
|
||||
currency: 'EUR',
|
||||
//percentage: 0,
|
||||
budgetedAmount: 0.0,
|
||||
spentAmount: 0.0,
|
||||
range: {
|
||||
start: null,
|
||||
end: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
name: "SpendInsightBox",
|
||||
computed: {
|
||||
percentage: function () {
|
||||
if (0 === this.budgetedAmount) {
|
||||
return 100;
|
||||
}
|
||||
if (0.0 === this.spentAmount) {
|
||||
return 0;
|
||||
}
|
||||
const pct = (this.spentAmount / this.budgetedAmount) * 100;
|
||||
if (pct > 100) {
|
||||
return 100;
|
||||
}
|
||||
return pct;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.store = useFireflyIIIStore();
|
||||
|
||||
// TODO this code snippet is recycled a lot.
|
||||
if (null === this.range.start || null === this.range.end) {
|
||||
// subscribe, then update:
|
||||
this.store.$onAction(
|
||||
({name, $store, args, after, onError,}) => {
|
||||
after((result) => {
|
||||
if (name === 'setRange') {
|
||||
this.range = result;
|
||||
this.triggerUpdate();
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
this.triggerUpdate();
|
||||
},
|
||||
methods: {
|
||||
triggerUpdate: function () {
|
||||
if (null !== this.store.getRange.start && null !== this.store.getRange.end) {
|
||||
this.budgeted = [];
|
||||
const start = new Date(this.store.getRange.start);
|
||||
const end = new Date(this.store.getRange.end);
|
||||
const sum = new Sum;
|
||||
this.currency = this.store.getCurrencyCode;
|
||||
sum.budgeted(start, end).then((response) => this.parseBudgetedResponse(response.data));
|
||||
//sum.paid(start, end).then((response) => this.parsePaidResponse(response.data));
|
||||
}
|
||||
},
|
||||
// TODO this method is recycled a lot.
|
||||
formatAmount: function (currencyCode, amount) {
|
||||
return Intl.NumberFormat(this.store.getLocale, {style: 'currency', currency: currencyCode}).format(amount);
|
||||
},
|
||||
parseBudgetedResponse: function (data) {
|
||||
for (let i in data) {
|
||||
if (data.hasOwnProperty(i)) {
|
||||
const current = data[i];
|
||||
const hasNative = current.native_id !== current.id && parseFloat(current.native_sum) !== 0.0;
|
||||
this.budgeted.push(
|
||||
{
|
||||
sum: current.sum,
|
||||
code: current.code,
|
||||
native: hasNative
|
||||
}
|
||||
);
|
||||
if (hasNative || current.native_id === current.id) {
|
||||
this.budgetedAmount = this.budgetedAmount + parseFloat(current.native_sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@@ -27,7 +27,7 @@
|
||||
<BillInsightBox />
|
||||
</div>
|
||||
<div class="col">
|
||||
TODO spend insight
|
||||
<SpendInsightBox />
|
||||
</div>
|
||||
<div class="col">
|
||||
TODO net worth insight
|
||||
@@ -94,6 +94,7 @@ export default {
|
||||
name: "Dashboard",
|
||||
components: {
|
||||
BillInsightBox: defineAsyncComponent(() => import('../../components/dashboard/BillInsightBox.vue')),
|
||||
SpendInsightBox: defineAsyncComponent(() => import('../../components/dashboard/SpendInsightBox.vue')),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
94
frontend/src/store/fireflyiii/actions.js
vendored
94
frontend/src/store/fireflyiii/actions.js
vendored
@@ -36,97 +36,3 @@ export function resetRange(context) {
|
||||
context.commit('setRange', defaultRange);
|
||||
}
|
||||
|
||||
export function setDatesFromViewRange(context) {
|
||||
let start;
|
||||
let end;
|
||||
let viewRange = context.getters.getViewRange;
|
||||
|
||||
let today = new Date;
|
||||
switch (viewRange) {
|
||||
case 'last365':
|
||||
start = startOfDay(subDays(today, 365));
|
||||
end = endOfDay(today);
|
||||
break;
|
||||
case 'last90':
|
||||
start = startOfDay(subDays(today, 90));
|
||||
end = endOfDay(today);
|
||||
break;
|
||||
case 'last30':
|
||||
start = startOfDay(subDays(today, 30));
|
||||
end = endOfDay(today);
|
||||
break;
|
||||
case 'last7':
|
||||
start = startOfDay(subDays(today, 7));
|
||||
end = endOfDay(today);
|
||||
break;
|
||||
case 'YTD':
|
||||
start = startOfYear(today);
|
||||
end = endOfDay(today);
|
||||
break;
|
||||
case 'QTD':
|
||||
start = startOfQuarter(today);
|
||||
end = endOfDay(today);
|
||||
break;
|
||||
case 'MTD':
|
||||
start = startOfMonth(today);
|
||||
end = endOfDay(today);
|
||||
break;
|
||||
case '1D':
|
||||
// today:
|
||||
start = startOfDay(today);
|
||||
end = endOfDay(today);
|
||||
break;
|
||||
case '1W':
|
||||
// this week:
|
||||
start = startOfDay(startOfWeek(today, {weekStartsOn: 1}));
|
||||
end = endOfDay(endOfWeek(today, {weekStartsOn: 1}));
|
||||
break;
|
||||
case '1M':
|
||||
// this month:
|
||||
start = startOfDay(startOfMonth(today));
|
||||
end = endOfDay(endOfMonth(today));
|
||||
break;
|
||||
case '3M':
|
||||
// this quarter
|
||||
start = startOfDay(startOfQuarter(today));
|
||||
end = endOfDay(endOfQuarter(today));
|
||||
break;
|
||||
case '6M':
|
||||
// this half-year
|
||||
if (today.getMonth() <= 5) {
|
||||
start = new Date(today);
|
||||
start.setMonth(0);
|
||||
start.setDate(1);
|
||||
start = startOfDay(start);
|
||||
end = new Date(today);
|
||||
end.setMonth(5);
|
||||
end.setDate(30);
|
||||
end = endOfDay(start);
|
||||
}
|
||||
if (today.getMonth() > 5) {
|
||||
start = new Date(today);
|
||||
start.setMonth(6);
|
||||
start.setDate(1);
|
||||
start = startOfDay(start);
|
||||
end = new Date(today);
|
||||
end.setMonth(11);
|
||||
end.setDate(31);
|
||||
end = endOfDay(start);
|
||||
}
|
||||
break;
|
||||
case '1Y':
|
||||
// this year
|
||||
start = new Date(today);
|
||||
start.setMonth(0);
|
||||
start.setDate(1);
|
||||
start = startOfDay(start);
|
||||
|
||||
end = new Date(today);
|
||||
end.setMonth(11);
|
||||
end.setDate(31);
|
||||
end = endOfDay(end);
|
||||
break;
|
||||
}
|
||||
context.commit('setRange', {start: start, end: end});
|
||||
context.commit('setDefaultRange', {start: start, end: end});
|
||||
}
|
||||
|
28
frontend/src/stores/fireflyiii.js
vendored
28
frontend/src/stores/fireflyiii.js
vendored
@@ -12,19 +12,31 @@ import {
|
||||
subDays
|
||||
} from "date-fns";
|
||||
|
||||
export const useFireflyIIIStore = defineStore('counter', {
|
||||
export const useFireflyIIIStore = defineStore('firefly-iii', {
|
||||
state: () => ({
|
||||
drawerState: true, viewRange: '1M', listPageSize: 10, range: {
|
||||
drawerState: true,
|
||||
viewRange: '1M',
|
||||
listPageSize: 10,
|
||||
locale: 'en-US',
|
||||
range: {
|
||||
start: null, end: null
|
||||
}, defaultRange: {
|
||||
start: null, end: null
|
||||
}, currencyCode: 'AAA', currencyId: '0', cacheKey: 'initial'
|
||||
},
|
||||
defaultRange: {
|
||||
start: null,
|
||||
end: null
|
||||
},
|
||||
currencyCode: 'AAA',
|
||||
currencyId: '0',
|
||||
cacheKey: 'initial'
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getViewRange(state) {
|
||||
return state.viewRange;
|
||||
},
|
||||
getLocale(state) {
|
||||
return state.locale;
|
||||
},
|
||||
|
||||
getListPageSize(state) {
|
||||
return state.listPageSize;
|
||||
@@ -156,9 +168,6 @@ export const useFireflyIIIStore = defineStore('counter', {
|
||||
|
||||
// mutators
|
||||
|
||||
increment() {
|
||||
this.counter++
|
||||
},
|
||||
updateViewRange(viewRange) {
|
||||
this.viewRange = viewRange;
|
||||
},
|
||||
@@ -166,6 +175,9 @@ export const useFireflyIIIStore = defineStore('counter', {
|
||||
updateListPageSize(value) {
|
||||
this.listPageSize = value;
|
||||
},
|
||||
setLocale(value) {
|
||||
this.locale = value;
|
||||
},
|
||||
|
||||
setRange(value) {
|
||||
this.range = value;
|
||||
|
Reference in New Issue
Block a user