From 3925077223e639c77faaa03222a2ef3695a92d70 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 9 Aug 2023 14:13:32 +0200 Subject: [PATCH] Expand views --- .../v2/api/v2/model/subscription/get.js | 42 ++++ resources/assets/v2/dashboard.js | 3 +- .../assets/v2/pages/dashboard/accounts.js | 3 +- resources/assets/v2/pages/dashboard/sankey.js | 188 +++++++++++------- .../v2/pages/dashboard/subscriptions.js | 154 ++++++++++++++ .../v2/support/default-chart-settings.js | 8 + resources/views/v2/index.blade.php | 6 +- 7 files changed, 322 insertions(+), 82 deletions(-) create mode 100644 resources/assets/v2/api/v2/model/subscription/get.js create mode 100644 resources/assets/v2/pages/dashboard/subscriptions.js diff --git a/resources/assets/v2/api/v2/model/subscription/get.js b/resources/assets/v2/api/v2/model/subscription/get.js new file mode 100644 index 0000000000..af4308c7f4 --- /dev/null +++ b/resources/assets/v2/api/v2/model/subscription/get.js @@ -0,0 +1,42 @@ +/* + * get.js + * Copyright (c) 2023 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 . + */ + + +import {api} from "../../../../boot/axios"; + +export default class Get { + + /** + * + * @param params + * @returns {Promise>} + */ + get(params) { + return api.get('/api/v2/subscriptions', {params: params}); + } + + paid(params) { + return api.get('/api/v2/subscriptions/sum/paid', {params: params}); + } + + unpaid(params) { + return api.get('/api/v2/subscriptions/sum/unpaid', {params: params}); + } +} diff --git a/resources/assets/v2/dashboard.js b/resources/assets/v2/dashboard.js index 15d26e592e..28c041f87e 100644 --- a/resources/assets/v2/dashboard.js +++ b/resources/assets/v2/dashboard.js @@ -25,8 +25,9 @@ import accounts from './pages/dashboard/accounts.js'; import budgets from './pages/dashboard/budgets.js'; import categories from './pages/dashboard/categories.js'; import sankey from './pages/dashboard/sankey.js'; +import subscriptions from './pages/dashboard/subscriptions.js'; -const comps = {dates, boxes, accounts, budgets, categories, sankey}; +const comps = {dates, boxes, accounts, budgets, categories, sankey, subscriptions}; function loadPage(comps) { Object.keys(comps).forEach(comp => { diff --git a/resources/assets/v2/pages/dashboard/accounts.js b/resources/assets/v2/pages/dashboard/accounts.js index d64d9f81a4..73ebeb0f29 100644 --- a/resources/assets/v2/pages/dashboard/accounts.js +++ b/resources/assets/v2/pages/dashboard/accounts.js @@ -24,7 +24,8 @@ import {setVariable} from "../../store/set-variable.js"; import Dashboard from "../../api/v2/chart/account/dashboard.js"; import formatMoney from "../../util/format-money.js"; import Get from "../../api/v1/accounts/get.js"; -import Chart from "chart.js/auto"; +//import Chart from "chart.js/auto"; +import {Chart, LineController, LineElement, PointElement, CategoryScale, LinearScale} from "chart.js"; // this is very ugly, but I have no better ideas at the moment to save the currency info // for each series. diff --git a/resources/assets/v2/pages/dashboard/sankey.js b/resources/assets/v2/pages/dashboard/sankey.js index b33ade3c25..974d1857a6 100644 --- a/resources/assets/v2/pages/dashboard/sankey.js +++ b/resources/assets/v2/pages/dashboard/sankey.js @@ -27,46 +27,72 @@ Chart.register(SankeyController, Flow); let currencies = []; -let chart = null; +let chart = null; let transactions = []; // little helper -function getObjectName(type, name, direction) { +function getObjectName(type, name, direction, code) { // category 4x if ('category' === type && null !== name && 'in' === direction) { - return 'Category "' + name + '" (in)'; + return 'Category "' + name + '" (in ' + code + ')'; } if ('category' === type && null === name && 'in' === direction) { - return 'Unknown category (in)'; + return 'Unknown category (in ' + code + ')'; } if ('category' === type && null !== name && 'out' === direction) { - return 'Category "' + name + '" (out)'; + return 'Category "' + name + '" (out ' + code + ')'; } if ('category' === type && null === name && 'out' === direction) { - return 'Unknown category (out)'; + return 'Unknown category (out ' + code + ')'; } - // category 4x + // account 4x if ('account' === type && null === name && 'in' === direction) { - return 'Unknown source account'; + return 'Unknown source account ' + code + ''; } if ('account' === type && null !== name && 'in' === direction) { - return name + ' (in)'; + return name + ' (in ' + code + ')'; } if ('account' === type && null === name && 'out' === direction) { - return 'Unknown destination account'; + return 'Unknown destination account ' + code + ''; } if ('account' === type && null !== name && 'out' === direction) { - return name + ' (out)'; + return name + ' (out ' + code + ')'; } - // budget 4x + // budget 2x if ('budget' === type && null !== name && 'out' === direction) { - return 'Budget "' + name + '" (out)'; + return 'Budget "' + name + '" (out ' + code + ')'; } if ('budget' === type && null === name && 'out' === direction) { - return 'Unknown budget'; + return 'Unknown budget (' + code + ')'; } - console.error('Cannot handle: type:"' + type + '",dir: "' + direction + '"'); + console.error('Cannot handle: type:"' + type + '", dir: "' + direction + '"'); +} + +function getLabelName(type, name, code) { + // category + if ('category' === type && null !== name) { + return 'Category "' + name + '" (' + code + ')'; + } + if ('category' === type && null === name) { + return 'Unknown category (' + code + ')'; + } + // account + if ('account' === type && null === name) { + return 'Unknown account (' + code + ')'; + } + if ('account' === type && null !== name) { + return name + ' (' + code + ')'; + } + + // budget 2x + if ('budget' === type && null !== name) { + return 'Budget "' + name + '" (' + code + ')'; + } + if ('budget' === type && null === name) { + return 'Unknown budget (' + code + ')'; + } + console.error('Cannot handle: type:"' + type + '"'); } export default () => ({ @@ -74,38 +100,38 @@ export default () => ({ autoConversion: false, sankeyGrouping: 'account', generateOptions(data) { - currencies = []; - console.log('generate options'); let options = getDefaultChartSettings('sankey'); - // temp code for first sankey - const colors = { - a: 'red', - b: 'green', - c: 'blue', - d: 'gray' - }; - const getColor = (key) => colors[key]; - // end of temp code for first sankey - + // reset currencies + currencies = []; + // variables collected for the sankey chart: let amounts = {}; - let sort = '10'; - let bigBox = 'TODO All money'; + let bigBox = 'TODO All money'; + let labels = {}; for (let i in transactions) { if (transactions.hasOwnProperty(i)) { let group = transactions[i]; for (let ii in group.attributes.transactions) { if (group.attributes.transactions.hasOwnProperty(ii)) { - let transaction = group.attributes.transactions[ii]; - let amount = this.autoConversion ? parseFloat(transaction.native_amount) : parseFloat(transaction.amount); - console.log(transaction); + // properties of the transaction, used in the generation of the chart: + let transaction = group.attributes.transactions[ii]; + let currencyCode = this.autoConversion ? transaction.native_code : transaction.currency_code; + let amount = this.autoConversion ? parseFloat(transaction.native_amount) : parseFloat(transaction.amount); let flowKey; + + /* + Two entries in the sankey diagram for deposits: + 1. From the revenue account (source) to a category (in). + 2. From the category (in) to the big inbox. + */ if ('deposit' === transaction.type) { - let category = getObjectName('category', transaction.category_name, 'in'); - let revenueAccount = getObjectName('account', transaction.source_name, 'in'); - // first: money flows from a revenue account to a category. - flowKey = sort + '-' + revenueAccount + '-' + category; + // nr 1 + let category = getObjectName('category', transaction.category_name, 'in', currencyCode); + let revenueAccount = getObjectName('account', transaction.source_name, 'in', currencyCode); + labels[category] = getLabelName('category', transaction.category_name, currencyCode); + labels[revenueAccount] = getLabelName('account', transaction.source_name, currencyCode); + flowKey = revenueAccount + '-' + category + '-' + currencyCode; if (!amounts.hasOwnProperty(flowKey)) { amounts[flowKey] = { from: revenueAccount, @@ -115,8 +141,8 @@ export default () => ({ } amounts[flowKey].amount += amount; - // second: money flows from category to the big inbox. - flowKey = sort + '-' + category + '-' + bigBox; + // nr 2 + flowKey = category + '-' + bigBox + '-' + currencyCode; if (!amounts.hasOwnProperty(flowKey)) { amounts[flowKey] = { from: category, @@ -126,11 +152,17 @@ export default () => ({ } amounts[flowKey].amount += amount; } + /* + Three entries in the sankey diagram for withdrawals: + 1. From the big box to a budget. + 2. From a budget to a category. + 3. From a category to an expense account. + */ if ('withdrawal' === transaction.type) { - sort = '11'; - // from bigBox to budget - let budget = getObjectName('budget', transaction.budget_name, 'out'); - flowKey = sort + '-' + bigBox + '-' + budget; + // 1. + let budget = getObjectName('budget', transaction.budget_name, 'out', currencyCode); + labels[budget] = getLabelName('budget', transaction.budget_name, currencyCode); + flowKey = bigBox + '-' + budget + '-' + currencyCode; if (!amounts.hasOwnProperty(flowKey)) { amounts[flowKey] = { @@ -142,9 +174,10 @@ export default () => ({ amounts[flowKey].amount += amount; - // then, it goes from a budget (in) to a category (out) - let category = getObjectName('category', transaction.category_name, 'out'); - flowKey = sort + '-' + budget + '-' + category; + // 2. + let category = getObjectName('category', transaction.category_name, 'out', currencyCode); + labels[category] = getLabelName('category', transaction.category_name, currencyCode); + flowKey = budget + '-' + category + '-' + currencyCode; if (!amounts.hasOwnProperty(flowKey)) { amounts[flowKey] = { @@ -155,9 +188,10 @@ export default () => ({ } amounts[flowKey].amount += amount; - // if set, from a category (in) to a specific revenue account (out) - let expenseAccount = getObjectName('account', transaction.destination_name, 'out'); - flowKey = sort + '-' + category + '-' + expenseAccount; + // 3. + let expenseAccount = getObjectName('account', transaction.destination_name, 'out', currencyCode); + labels[expenseAccount] = getLabelName('account', transaction.destination_name, currencyCode); + flowKey = category + '-' + expenseAccount + '-' + currencyCode; if (!amounts.hasOwnProperty(flowKey)) { amounts[flowKey] = { @@ -174,32 +208,32 @@ export default () => ({ } let dataSet = - // sankey chart has one data set. - { - label: 'My sankey', - data: [], - //colorFrom: (c) => getColor(c.dataset.data[c.dataIndex].from), - //colorTo: (c) => getColor(c.dataset.data[c.dataIndex].to), - colorMode: 'gradient', // or 'from' or 'to' - /* optional labels */ - // labels: { - // a: 'Label A', - // b: 'Label B', - // c: 'Label C', - // d: 'Label D' - // }, - /* optional priority */ - // priority: { - // b: 1, - // d: 0 - // }, - /* optional column overrides */ - // column: { - // d: 1 - // }, - size: 'max', // or 'min' if flow overlap is preferred - }; - + // sankey chart has one data set. + { + label: 'My sankey', + data: [], + //colorFrom: (c) => getColor(c.dataset.data[c.dataIndex].from), + //colorTo: (c) => getColor(c.dataset.data[c.dataIndex].to), + colorMode: 'gradient', // or 'from' or 'to' + labels: labels, + /* optional labels */ + // labels: { + // a: 'Label A', + // b: 'Label B', + // c: 'Label C', + // d: 'Label D' + // }, + /* optional priority */ + // priority: { + // b: 1, + // d: 0 + // }, + /* optional column overrides */ + // column: { + // d: 1 + // }, + size: 'max', // or 'min' if flow overlap is preferred + }; for (let i in amounts) { if (amounts.hasOwnProperty(i)) { let amount = amounts[i]; @@ -229,7 +263,7 @@ export default () => ({ this.downloadTransactions(params); }, downloadTransactions(params) { - console.log('Downloading page ' + params.page + '...'); + //console.log('Downloading page ' + params.page + '...'); const getter = new Get(); getter.get(params).then((response) => { transactions = [...transactions, ...response.data.data]; @@ -242,8 +276,8 @@ export default () => ({ return; } // continue to next step. - console.log('Final page!'); - console.log(transactions); + //console.log('Final page!'); + //console.log(transactions); this.drawChart(this.generateOptions()); this.loading = false; }); diff --git a/resources/assets/v2/pages/dashboard/subscriptions.js b/resources/assets/v2/pages/dashboard/subscriptions.js new file mode 100644 index 0000000000..13992a1ba5 --- /dev/null +++ b/resources/assets/v2/pages/dashboard/subscriptions.js @@ -0,0 +1,154 @@ +/* + * budgets.js + * Copyright (c) 2023 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 . + */ +import {getVariable} from "../../store/get-variable.js"; +import Get from "../../api/v2/model/subscription/get.js"; +import Chart from 'chart.js/auto'; +import {getDefaultChartSettings} from "../../support/default-chart-settings.js"; +import formatMoney from "../../util/format-money.js"; +import {format} from "date-fns"; + +let currencies = []; +let chart = null; +let chartData = null; + +export default () => ({ + loading: false, + autoConversion: false, + loadChart() { + if (true === this.loading) { + return; + } + this.loading = true; + + if (null !== chartData) { + this.drawChart(this.generateOptions(chartData)); + this.loading = false; + return; + } + this.getFreshData(); + }, + drawChart(options) { + if (null !== chart) { + chart.data.datasets = options.data.datasets; + chart.update(); + return; + } + chart = new Chart(document.querySelector("#subscriptions-chart"), options); + }, + getFreshData() { + const getter = new Get(); + let params = { + start: format(new Date(window.store.get('start')), 'y-MM-dd'), + end: format(new Date(window.store.get('end')), 'y-MM-dd') + }; + + getter.paid(params).then((response) => { + let paidData = response.data; + getter.unpaid(params).then((response) => { + let unpaidData = response.data; + let chartData = {paid: paidData, unpaid: unpaidData}; + this.drawChart(this.generateOptions(chartData)); + this.loading = false; + }); + }); + }, + generateOptions(data) { + let options = getDefaultChartSettings('pie'); + console.log(data); + options.data.labels = ['TODO paid', 'TODO unpaid']; + options.data.datasets = []; + let collection = {}; + for (let i in data.paid) { + if (data.paid.hasOwnProperty(i)) { + let current = data.paid[i]; + let currencyCode = this.autoConversion ? current.native_code : current.currency_code; + let amount = this.autoConversion ? current.native_sum : current.sum; + if (!collection.hasOwnProperty(currencyCode)) { + collection[currencyCode] = { + paid: 0, + unpaid: 0, + }; + } + // in case of paid, add to "paid": + collection[currencyCode].paid += (parseFloat(amount) * -1); + } + } + // unpaid + for (let i in data.unpaid) { + if (data.unpaid.hasOwnProperty(i)) { + let current = data.unpaid[i]; + let currencyCode = this.autoConversion ? current.native_code : current.currency_code; + let amount = this.autoConversion ? current.native_sum : current.sum; + if (!collection.hasOwnProperty(currencyCode)) { + collection[currencyCode] = { + paid: 0, + unpaid: 0, + }; + } + console.log(current); + // in case of paid, add to "paid": + collection[currencyCode].unpaid += parseFloat(amount); + } + } + for (let currencyCode in collection) { + if (collection.hasOwnProperty(currencyCode)) { + let current = collection[currencyCode]; + options.data.datasets.push( + { + label: currencyCode, + data: [current.paid, current.unpaid], + backgroundColor: [ + 'rgb(54, 162, 235)', // green (paid) + 'rgb(255, 99, 132)', // red (unpaid_ + ], + //hoverOffset: 4 + } + ) + } + } + + return options; + }, + + + init() { + Promise.all([getVariable('autoConversion', false),]).then((values) => { + this.autoConversion = values[0]; + if (false === this.loading) { + this.loadChart(); + } + }); + window.store.observe('end', () => { + if (false === this.loading) { + this.chartData = null; + this.loadChart(); + } + }); + window.store.observe('autoConversion', (newValue) => { + this.autoConversion = newValue; + if (false === this.loading) { + this.loadChart(); + } + }); + }, + +}); + + diff --git a/resources/assets/v2/support/default-chart-settings.js b/resources/assets/v2/support/default-chart-settings.js index 9524905a80..a1c5e06044 100644 --- a/resources/assets/v2/support/default-chart-settings.js +++ b/resources/assets/v2/support/default-chart-settings.js @@ -27,6 +27,14 @@ function getDefaultChartSettings(type) { } } } + if ('pie' === type) { + return { + type: 'pie', + data: { + datasets: [], + } + } + } if ('column' === type) { return { type: 'bar', diff --git a/resources/views/v2/index.blade.php b/resources/views/v2/index.blade.php index 8dc3020ba6..21f58b57b1 100644 --- a/resources/views/v2/index.blade.php +++ b/resources/views/v2/index.blade.php @@ -161,10 +161,10 @@
-
- +
+