Various code for currency exchange rate support

This commit is contained in:
James Cole
2022-06-06 14:40:19 +02:00
parent 9c08b9f1d3
commit d007db166a
24 changed files with 987 additions and 188 deletions

View File

@@ -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
View 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}});
// }*/
}

View 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});
// }
}

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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});
}

View File

@@ -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;