Expand layout

This commit is contained in:
James Cole
2023-08-06 18:33:29 +02:00
parent e1915e365a
commit 551408b801
20 changed files with 869 additions and 481 deletions

View File

@@ -0,0 +1,30 @@
/*
* overview.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 Dashboard {
dashboard(start, end) {
let startStr = format(start, 'y-MM-dd');
let endStr = format(end, 'y-MM-dd');
return api.get('/api/v2/chart/category/dashboard', {params: {start: startStr, end: endStr}});
}
}

View File

@@ -23,11 +23,13 @@ import dates from './pages/shared/dates.js';
import boxes from './pages/dashboard/boxes.js';
import accounts from './pages/dashboard/accounts.js';
import budgets from './pages/dashboard/budgets.js';
import categories from './pages/dashboard/categories.js';
const comps = {dates, boxes, accounts, budgets};
const comps = {dates, boxes, accounts, budgets, categories};
function loadPage(comps) {
Object.keys(comps).forEach(comp => {
console.log(`Loading ${comp}`);
let data = comps[comp]();
Alpine.data(comp, () => data);
});
@@ -36,11 +38,11 @@ function loadPage(comps) {
// wait for load until bootstrapped event is received.
document.addEventListener('firefly-iii-bootstrapped', () => {
console.log('Loaded through event listener.');
//console.log('Loaded through event listener.');
loadPage(comps);
});
// or is bootstrapped before event is triggered.
if (window.bootstrapped) {
console.log('Loaded through window variable.');
//console.log('Loaded through window variable.');
loadPage(comps);
}

View File

@@ -37,106 +37,119 @@ export default () => ({
accountList: [],
autoConversion: false,
chart: null,
chartData: null,
chartOptions: null,
switchAutoConversion() {
this.autoConversion = !this.autoConversion;
setVariable('autoConversion', this.autoConversion);
this.loadChart();
},
loadChart() {
console.log('loadChart.');
if (true === this.loading) {
console.log('loadChart CANCELLED');
return;
}
console.log('loadChart continues');
// load chart data
this.loading = true;
getFreshData() {
const dashboard = new Dashboard();
dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => {
// chart options (may need to be centralized later on)
window.currencies = [];
let options = {
legend: {show: false},
chart: {
height: 400,
toolbar: {tools: {zoom: false, download: false, pan: false}},
type: 'line'
}, series: [],
settings: [],
xaxis: {
categories: [],
labels: {
formatter: function (value) {
if (undefined === value) {
return '';
}
const date = new Date(value);
if (date instanceof Date && !isNaN(date)) {
return formatLocal(date, 'PP');
}
console.error('Could not parse "' + value + '", return "".');
return ':(';
}
}
}, yaxis: {
labels: {
formatter: function (value, index) {
if (undefined === value) {
return value;
}
if (undefined === index) {
return value;
}
if (typeof index === 'object') {
index = index.seriesIndex;
}
//console.log(index);
let currencyCode = window.currencies[index] ?? 'EUR';
return formatMoney(value, currencyCode);
}
}
},
};
// render data:
for (let i = 0; i < response.data.length; i++) {
if (response.data.hasOwnProperty(i)) {
let current = response.data[i];
let entry = [];
let collection = [];
// use the "native" currency code and use the "native_entries" as array
if (this.autoConversion) {
window.currencies.push(current.native_code);
collection = current.native_entries;
}
if (!this.autoConversion) {
window.currencies.push(current.currency_code);
collection = current.entries;
}
for (const [ii, value] of Object.entries(collection)) {
entry.push({x: format(new Date(ii), 'yyyy-MM-dd'), y: parseFloat(value)});
}
options.series.push({name: current.label, data: entry});
}
}
if (null !== this.chart) {
// chart already in place, refresh:
this.chart.updateOptions(options);
}
if (null === this.chart) {
this.chart = new ApexCharts(document.querySelector("#account-chart"), options);
this.chart.render();
}
this.loading = false;
this.chartData = response.data;
this.generateOptions(this.chartData);
this.drawChart();
});
}, loadAccounts() {
console.log('loadAccounts');
if (true === this.loadingAccounts) {
console.log('loadAccounts CANCELLED');
},
generateOptions(data) {
window.currencies = [];
let options = {
legend: {show: false},
chart: {
height: 400,
type: 'line'
},
series: [],
settings: [],
xaxis: {
categories: [],
labels: {
formatter: function (value) {
if (undefined === value) {
return '';
}
const date = new Date(value);
if (date instanceof Date && !isNaN(date)) {
return formatLocal(date, 'PP');
}
console.error('Could not parse "' + value + '", return "".');
return ':(';
}
}
},
yaxis: {
labels: {
formatter: function (value, index) {
if (undefined === value) {
return value;
}
if (undefined === index) {
return value;
}
if (typeof index === 'object') {
index = index.seriesIndex;
}
//console.log(index);
let currencyCode = window.currencies[index] ?? 'EUR';
return formatMoney(value, currencyCode);
}
}
},
};
// render data:
for (let i = 0; i < data.length; i++) {
if (data.hasOwnProperty(i)) {
let current = data[i];
let entry = [];
let collection = [];
// use the "native" currency code and use the "native_entries" as array
if (this.autoConversion) {
window.currencies.push(current.native_code);
collection = current.native_entries;
}
if (!this.autoConversion) {
window.currencies.push(current.currency_code);
collection = current.entries;
}
for (const [ii, value] of Object.entries(collection)) {
entry.push({x: format(new Date(ii), 'yyyy-MM-dd'), y: parseFloat(value)});
}
options.series.push({name: current.label, data: entry});
}
}
this.chartOptions = options;
},
loadChart() {
if (true === this.loading) {
return;
}
this.loading = true;
if (null === this.chartData) {
this.getFreshData();
}
if (null !== this.chartData) {
this.generateOptions(this.chartData);
this.drawChart();
}
this.loading = false;
},
drawChart() {
if (null !== this.chart) {
// chart already in place, refresh:
this.chart.updateOptions(this.chartOptions);
}
if (null === this.chart) {
this.chart = new ApexCharts(document.querySelector("#account-chart"), this.chartOptions);
this.chart.render();
}
},
loadAccounts() {
if (true === this.loadingAccounts) {
return;
}
console.log('loadAccounts continues');
this.loadingAccounts = true;
const max = 10;
let totalAccounts = 0;
@@ -144,7 +157,6 @@ export default () => ({
let accounts = [];
Promise.all([getVariable('frontpageAccounts'),]).then((values) => {
totalAccounts = values[0].length;
console.log('total accounts is ' + totalAccounts);
for (let i in values[0]) {
let account = values[0];
if (account.hasOwnProperty(i)) {
@@ -186,7 +198,6 @@ export default () => ({
if (count === totalAccounts) {
this.accountList = accounts;
}
console.log('Count is now ' + count);
});
});
}
@@ -196,16 +207,14 @@ export default () => ({
},
init() {
console.log('init accounts');
Promise.all([getVariable('viewRange', '1M'), getVariable('autoConversion', false),]).then((values) => {
console.log('from promise');
this.autoConversion = values[1];
// console.log(values[1]);
this.loadChart();
this.loadAccounts();
});
window.store.observe('end', () => {
console.log('from observe end');
this.chartData = null;
this.loadChart();
this.loadAccounts();
});

View File

@@ -31,149 +31,164 @@ export default () => ({
netBox: {net: []},
autoConversion: false,
loading: false,
loadBoxes() {
if (this.loading) {
return;
}
this.loading = true;
boxData: null,
boxOptions: null,
getFreshData() {
// get stuff
let getter = new Summary();
let start = new Date(window.store.get('start'));
let end = new Date(window.store.get('end'));
getter.get(format(start, 'yyyy-MM-dd'), format(end, 'yyyy-MM-dd'), null).then((response) => {
// reset boxes:
this.balanceBox = {amounts: [], subtitles: []};
this.billBox = {paid: [], unpaid: []};
this.leftBox = {left: [], perDay: []};
this.netBox = {net: []};
let subtitles = {};
// process new content:
for (const i in response.data) {
if (response.data.hasOwnProperty(i)) {
const current = response.data[i];
let key = current.key;
// native (auto conversion):
if (this.autoConversion) {
if (key.startsWith('balance-in-native')) {
this.balanceBox.amounts.push(formatMoney(current.value, current.currency_code));
// prep subtitles (for later)
if (!subtitles.hasOwnProperty(current.currency_code)) {
subtitles[current.currency_code] = '';
}
continue;
}
// spent info is used in subtitle:
if (key.startsWith('spent-in-native')) {
// prep subtitles (for later)
if (!subtitles.hasOwnProperty(current.currency_code)) {
subtitles[current.currency_code] = '';
}
// append the amount spent.
subtitles[current.currency_code] =
subtitles[current.currency_code] +
formatMoney(current.value, current.currency_code);
continue;
}
// earned info is used in subtitle:
if (key.startsWith('earned-in-native')) {
// prep subtitles (for later)
if (!subtitles.hasOwnProperty(current.currency_code)) {
subtitles[current.currency_code] = '';
}
// prepend the amount earned.
subtitles[current.currency_code] =
formatMoney(current.value, current.currency_code) + ' + ' +
subtitles[current.currency_code];
continue;
}
if (key.startsWith('bills-unpaid-in-native')) {
this.billBox.unpaid.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('bills-paid-in-native')) {
this.billBox.paid.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('left-to-spend-in-native')) {
this.leftBox.left.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('left-per-day-to-spend-in-native')) { // per day
this.leftBox.perDay.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('net-worth-in-native')) {
this.netBox.net.push(formatMoney(current.value, current.currency_code));
continue;
}
}
// not native
if (!this.autoConversion && !key.endsWith('native')) {
if (key.startsWith('balance-in-')) {
this.balanceBox.amounts.push(formatMoney(current.value, current.currency_code));
continue;
}
// spent info is used in subtitle:
if (key.startsWith('spent-in-')) {
// prep subtitles (for later)
if (!subtitles.hasOwnProperty(current.currency_code)) {
subtitles[current.currency_code] = '';
}
// append the amount spent.
subtitles[current.currency_code] =
subtitles[current.currency_code] +
formatMoney(current.value, current.currency_code);
continue;
}
// earned info is used in subtitle:
if (key.startsWith('earned-in-')) {
// prep subtitles (for later)
if (!subtitles.hasOwnProperty(current.currency_code)) {
subtitles[current.currency_code] = '';
}
// prepend the amount earned.
subtitles[current.currency_code] =
formatMoney(current.value, current.currency_code) + ' + ' +
subtitles[current.currency_code];
continue;
}
if (key.startsWith('bills-unpaid-in-')) {
this.billBox.unpaid.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('bills-paid-in-')) {
this.billBox.paid.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('left-to-spend-in-')) {
this.leftBox.left.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('left-per-day-to-spend-in-')) {
this.leftBox.perDay.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('net-worth-in-')) {
this.netBox.net.push(formatMoney(current.value, current.currency_code));
}
}
}
}
for (let i in subtitles) {
if (subtitles.hasOwnProperty(i)) {
this.balanceBox.subtitles.push(subtitles[i]);
}
}
this.loading = false;
this.boxData = response.data;
this.generateOptions(this.boxData);
//this.drawChart();
});
},
generateOptions(data) {
this.balanceBox = {amounts: [], subtitles: []};
this.billBox = {paid: [], unpaid: []};
this.leftBox = {left: [], perDay: []};
this.netBox = {net: []};
let subtitles = {};
// process new content:
for (const i in data) {
if (data.hasOwnProperty(i)) {
const current = data[i];
let key = current.key;
// native (auto conversion):
if (this.autoConversion) {
if (key.startsWith('balance-in-native')) {
this.balanceBox.amounts.push(formatMoney(current.value, current.currency_code));
// prep subtitles (for later)
if (!subtitles.hasOwnProperty(current.currency_code)) {
subtitles[current.currency_code] = '';
}
continue;
}
// spent info is used in subtitle:
if (key.startsWith('spent-in-native')) {
// prep subtitles (for later)
if (!subtitles.hasOwnProperty(current.currency_code)) {
subtitles[current.currency_code] = '';
}
// append the amount spent.
subtitles[current.currency_code] =
subtitles[current.currency_code] +
formatMoney(current.value, current.currency_code);
continue;
}
// earned info is used in subtitle:
if (key.startsWith('earned-in-native')) {
// prep subtitles (for later)
if (!subtitles.hasOwnProperty(current.currency_code)) {
subtitles[current.currency_code] = '';
}
// prepend the amount earned.
subtitles[current.currency_code] =
formatMoney(current.value, current.currency_code) + ' + ' +
subtitles[current.currency_code];
continue;
}
if (key.startsWith('bills-unpaid-in-native')) {
this.billBox.unpaid.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('bills-paid-in-native')) {
this.billBox.paid.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('left-to-spend-in-native')) {
this.leftBox.left.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('left-per-day-to-spend-in-native')) { // per day
this.leftBox.perDay.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('net-worth-in-native')) {
this.netBox.net.push(formatMoney(current.value, current.currency_code));
continue;
}
}
// not native
if (!this.autoConversion && !key.endsWith('native')) {
if (key.startsWith('balance-in-')) {
this.balanceBox.amounts.push(formatMoney(current.value, current.currency_code));
continue;
}
// spent info is used in subtitle:
if (key.startsWith('spent-in-')) {
// prep subtitles (for later)
if (!subtitles.hasOwnProperty(current.currency_code)) {
subtitles[current.currency_code] = '';
}
// append the amount spent.
subtitles[current.currency_code] =
subtitles[current.currency_code] +
formatMoney(current.value, current.currency_code);
continue;
}
// earned info is used in subtitle:
if (key.startsWith('earned-in-')) {
// prep subtitles (for later)
if (!subtitles.hasOwnProperty(current.currency_code)) {
subtitles[current.currency_code] = '';
}
// prepend the amount earned.
subtitles[current.currency_code] =
formatMoney(current.value, current.currency_code) + ' + ' +
subtitles[current.currency_code];
continue;
}
if (key.startsWith('bills-unpaid-in-')) {
this.billBox.unpaid.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('bills-paid-in-')) {
this.billBox.paid.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('left-to-spend-in-')) {
this.leftBox.left.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('left-per-day-to-spend-in-')) {
this.leftBox.perDay.push(formatMoney(current.value, current.currency_code));
continue;
}
if (key.startsWith('net-worth-in-')) {
this.netBox.net.push(formatMoney(current.value, current.currency_code));
}
}
}
}
for (let i in subtitles) {
if (subtitles.hasOwnProperty(i)) {
this.balanceBox.subtitles.push(subtitles[i]);
}
}
},
loadBoxes() {
if (true === this.loading) {
return;
}
this.loading = true;
if (null === this.boxData) {
this.getFreshData();
}
if (null !== this.boxData) {
this.generateOptions(this.boxData);
//this.drawChart();
}
this.loading = false;
},
// Getter
@@ -183,6 +198,7 @@ export default () => ({
this.loadBoxes();
});
window.store.observe('end', () => {
this.boxData = null;
this.loadBoxes();
});
window.store.observe('autoConversion', (newValue) => {

View File

@@ -24,158 +24,176 @@ import formatMoney from "../../util/format-money.js";
window.budgetCurrencies = [];
export default () => ({
loadingChart: false,
loading: false,
chart: null,
autoConversion: false,
chartData: null,
chartOptions: null,
loadChart() {
if (this.loadingChart) {
if (true === this.loading) {
return;
}
// load chart data
this.loadingChart = true;
window.budgetCurrencies = [];
this.loading = true;
if (null === this.chartData) {
this.getFreshData();
}
if (null !== this.chartData) {
this.generateOptions(this.chartData);
this.drawChart();
}
this.loading = false;
},
drawChart() {
if (null !== this.chart) {
// chart already in place, refresh:
this.chart.updateOptions(this.chartOptions);
}
if (null === this.chart) {
this.chart = new ApexCharts(document.querySelector("#budget-chart"), this.chartOptions);
this.chart.render();
}
},
getFreshData() {
const dashboard = new Dashboard();
dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => {
let options = {
legend: {show: false},
series: [{
name: 'Spent',
data: []
}, {
name: 'Left',
data: []
}, {
name: 'Overspent',
data: []
}],
chart: {
type: 'bar',
height: 400,
stacked: true,
toolbar: {tools: {zoom: false, download: false, pan: false}},
zoom: {
enabled: true
this.chartData = response.data;
this.generateOptions(this.chartData);
this.drawChart();
});
},
generateOptions(data) {
window.budgetCurrencies = [];
let options = {
legend: {show: false},
series: [{
name: 'Spent',
data: []
}, {
name: 'Left',
data: []
}, {
name: 'Overspent',
data: []
}],
chart: {
type: 'bar',
height: 400,
stacked: true,
toolbar: {tools: {zoom: false, download: false, pan: false}},
zoom: {
enabled: true
}
},
responsive: [{
breakpoint: 480,
options: {
legend: {
position: 'bottom',
offsetX: -10,
offsetY: 0
}
},
responsive: [{
breakpoint: 480,
options: {
legend: {
position: 'bottom',
offsetX: -10,
offsetY: 0
}
}
}],
plotOptions: {
bar: {
horizontal: false,
borderRadius: 10,
dataLabels: {
total: {
enabled: true,
style: {
fontSize: '13px',
fontWeight: 900
},
formatter: function (val, opt) {
let index = 0;
if (typeof opt === 'object') {
index = opt.dataPointIndex; // this is the "category name + currency" index
}
let currencyCode = window.budgetCurrencies[index] ?? 'EUR';
return formatMoney(val, currencyCode);
}
}],
plotOptions: {
bar: {
horizontal: false,
borderRadius: 10,
dataLabels: {
total: {
enabled: true,
// style: {
// fontSize: '13px',
// fontWeight: 900
// },
formatter: function (val, opt) {
let index = 0;
if (typeof opt === 'object') {
index = opt.dataPointIndex; // this is the "category name + currency" index
}
let currencyCode = window.budgetCurrencies[index] ?? 'EUR';
return formatMoney(val, currencyCode);
}
}
},
},
yaxis: {
labels: {
formatter: function (value, index) {
if (undefined === value) {
return value;
}
if (undefined === index) {
return value;
}
if (typeof index === 'object') {
index = index.dataPointIndex; // this is the "category name + currency" index
}
let currencyCode = window.budgetCurrencies[index] ?? 'EUR';
return formatMoney(value, currencyCode);
}
}
},
xaxis: {
categories: []
},
fill: {
opacity: 0.8
},
dataLabels: {
formatter: function (val, opt) {
let index = 0;
if (typeof opt === 'object') {
index = opt.dataPointIndex; // this is the "category name + currency" index
},
yaxis: {
labels: {
formatter: function (value, index) {
if (undefined === value) {
return value;
}
if (undefined === index) {
return value;
}
if (typeof index === 'object') {
index = index.dataPointIndex; // this is the "category name + currency" index
}
let currencyCode = window.budgetCurrencies[index] ?? 'EUR';
return formatMoney(val, currencyCode);
},
}
};
for (const i in response.data) {
if (response.data.hasOwnProperty(i)) {
let current = response.data[i];
// convert to EUR yes no?
let label = current.label + ' (' + current.currency_code + ')';
options.xaxis.categories.push(label);
if (this.autoConversion) {
window.budgetCurrencies.push(current.native_code);
// series 0: spent
options.series[0].data.push(parseFloat(current.native_entries.spent) * -1);
// series 1: left
options.series[1].data.push(parseFloat(current.native_entries.left));
// series 2: overspent
options.series[2].data.push(parseFloat(current.native_entries.overspent));
return formatMoney(value, currencyCode);
}
if (!this.autoConversion) {
window.budgetCurrencies.push(current.currency_code);
// series 0: spent
options.series[0].data.push(parseFloat(current.entries.spent) * -1);
// series 1: left
options.series[1].data.push(parseFloat(current.entries.left));
// series 2: overspent
options.series[2].data.push(parseFloat(current.entries.overspent));
}
}
},
xaxis: {
categories: []
},
fill: {
opacity: 0.8
},
dataLabels: {
formatter: function (val, opt) {
let index = 0;
if (typeof opt === 'object') {
index = opt.dataPointIndex; // this is the "category name + currency" index
}
let currencyCode = window.budgetCurrencies[index] ?? 'EUR';
return formatMoney(val, currencyCode);
},
}
};
if (null !== this.chart) {
// chart already in place, refresh:
this.chart.updateOptions(options);
for (const i in data) {
if (data.hasOwnProperty(i)) {
let current = data[i];
// convert to EUR yes no?
let label = current.label + ' (' + current.currency_code + ')';
options.xaxis.categories.push(label);
if (this.autoConversion) {
window.budgetCurrencies.push(current.native_code);
// series 0: spent
options.series[0].data.push(parseFloat(current.native_entries.spent) * -1);
// series 1: left
options.series[1].data.push(parseFloat(current.native_entries.left));
// series 2: overspent
options.series[2].data.push(parseFloat(current.native_entries.overspent));
}
if (!this.autoConversion) {
window.budgetCurrencies.push(current.currency_code);
// series 0: spent
options.series[0].data.push(parseFloat(current.entries.spent) * -1);
// series 1: left
options.series[1].data.push(parseFloat(current.entries.left));
// series 2: overspent
options.series[2].data.push(parseFloat(current.entries.overspent));
}
}
if (null === this.chart) {
this.chart = new ApexCharts(document.querySelector("#budget-chart"), options);
this.chart.render();
}
this.loadingChart = false;
});
}
this.chartOptions = options;
},
init() {
Promise.all([getVariable('autoConversion', false),]).then((values) => {
this.autoConversion = values[0];
this.loadChart();
});
// todo the charts don't need to reload from server if the autoConversion value changes.
window.store.observe('end', () => {
this.chartData = null;
this.loadChart();
});
window.store.observe('autoConversion', (newValue) => {

View File

@@ -0,0 +1,205 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {getVariable} from "../../store/get-variable.js";
import Dashboard from "../../api/v2/chart/category/dashboard.js";
import ApexCharts from "apexcharts";
import formatMoney from "../../util/format-money.js";
window.categoryCurrencies = [];
export default () => ({
loading: false,
chart: null,
autoConversion: false,
chartData: null,
chartOptions: null,
generateOptions(data) {
window.categoryCurrencies = [];
let options = {
series: [],
chart: {
type: 'bar',
height: 350
},
plotOptions: {
bar: {
horizontal: false,
columnWidth: '55%',
endingShape: 'rounded'
},
},
dataLabels: {
enabled: false
},
stroke: {
show: true,
width: 2,
colors: ['transparent']
},
xaxis: {
categories: [],
},
yaxis: {},
fill: {
opacity: 1
},
tooltip: {
y: {
formatter: function (value, index) {
if (undefined === value) {
return value;
}
if (undefined === index) {
return value;
}
if (typeof index === 'object') {
index = index.seriesIndex; // this is the currency index.
}
let currencyCode = window.categoryCurrencies[index] ?? 'EUR';
return formatMoney(value, currencyCode);
}
}
}
};
// first, collect all currencies and use them as series.
let series = {};
for (const i in data) {
if (data.hasOwnProperty(i)) {
let current = data[i];
let code = current.currency_code;
// only use native code when doing auto conversion.
if (this.autoConversion) {
code = current.native_code;
}
if (!series.hasOwnProperty(code)) {
series[code] = {
name: code,
data: {},
};
window.categoryCurrencies.push(code);
}
}
}
// loop data again to add amounts.
for (const i in data) {
if (data.hasOwnProperty(i)) {
let current = data[i];
let code = current.currency_code;
if (this.autoConversion) {
code = current.native_code;
}
// loop series, add 0 if not present or add actual amount.
for (const ii in series) {
if (series.hasOwnProperty(ii)) {
let amount = 0.0;
if (code === ii) {
// this series' currency matches this column's currency.
amount = parseFloat(current.amount);
if (this.autoConversion) {
amount = parseFloat(current.native_amount);
}
}
if (series[ii].data.hasOwnProperty(current.label)) {
// there is a value for this particular currency. The amount from this column will be added.
// (even if this column isn't recorded in this currency and a new filler value is written)
// this is so currency conversion works.
series[ii].data[current.label] = series[ii].data[current.label] + amount;
}
if (!series[ii].data.hasOwnProperty(current.label)) {
// this column's amount is not yet set in this series.
series[ii].data[current.label] = amount;
}
}
}
// add label to x-axis, not unimportant.
if (!options.xaxis.categories.includes(current.label)) {
options.xaxis.categories.push(current.label);
}
}
}
// loop the series and create Apex-compatible data sets.
for (const i in series) {
let current = {
name: i,
data: [],
}
for (const ii in series[i].data) {
current.data.push(series[i].data[ii]);
}
options.series.push(current);
}
this.chartOptions = options;
},
drawChart() {
if (null !== this.chart) {
// chart already in place, refresh:
this.chart.updateOptions(this.chartOptions);
}
if (null === this.chart) {
this.chart = new ApexCharts(document.querySelector("#category-chart"), this.chartOptions);
this.chart.render();
}
this.loading = false;
},
getFreshData() {
const dashboard = new Dashboard();
dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => {
this.chartData = response.data;
this.generateOptions(this.chartData);
this.drawChart();
});
},
loadChart() {
if (true === this.loading) {
return;
}
this.loading = true;
if (null === this.chartData) {
this.getFreshData();
}
if (null !== this.chartData) {
this.generateOptions(this.chartData);
this.drawChart();
}
this.loading = false;
},
init() {
Promise.all([getVariable('autoConversion', false),]).then((values) => {
this.autoConversion = values[0];
this.loadChart();
});
window.store.observe('end', () => {
this.chartData = null;
this.loadChart();
});
window.store.observe('autoConversion', (newValue) => {
this.autoConversion = newValue;
this.loadChart();
});
},
});

View File

@@ -9,102 +9,100 @@
<div class="container-fluid">
@include('partials.dashboard.boxes')
<!-- row with account data -->
<div>
<div class="row" x-data="accounts">
<div class="col-xl-8 col-lg-12 col-sm-12 col-xs-12">
<div class="row mb-2">
<div class="col">
<div class="row mb-2" x-data="accounts">
<div class="col-xl-8 col-lg-12 col-sm-12 col-xs-12">
<div class="row mb-2">
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title"><a href="{{ route('accounts.index',['asset']) }}"
title="{{ __('firefly.yourAccounts') }}">{{ __('firefly.yourAccounts') }}</a>
</h3>
</div>
<div class="card-body">
<div id="account-chart"></div>
<p class="text-end">
<template x-if="autoConversion">
<button type="button" @click="switchAutoConversion"
class="btn btn-outline-info btm-sm">
<div class="card">
<div class="card-header">
<h3 class="card-title"><a href="{{ route('accounts.index',['asset']) }}"
title="{{ __('firefly.yourAccounts') }}">{{ __('firefly.yourAccounts') }}</a>
</h3>
</div>
<div class="card-body">
<div id="account-chart"></div>
<p class="text-end">
<template x-if="autoConversion">
<button type="button" @click="switchAutoConversion"
class="btn btn-outline-info btm-sm">
<span
class="fa-solid fa-comments-dollar"></span> {{ __('firefly.disable_auto_convert') }}
</button>
</template>
<template x-if="!autoConversion">
<button type="button" @click="switchAutoConversion"
class="btn btn-outline-info btm-sm">
</button>
</template>
<template x-if="!autoConversion">
<button type="button" @click="switchAutoConversion"
class="btn btn-outline-info btm-sm">
<span
class="fa-solid fa-comments-dollar"></span> {{ __('firefly.enable_auto_convert') }}
</button>
</template>
</p>
</div>
</button>
</template>
</p>
</div>
</div>
</div>
<div class="row mb-2" x-data="budgets">
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title"><a href="{{ route('budgets.index') }}"
title="{{ __('firefly.go_to_budgets') }}">{{ __('firefly.go_to_budgets') }}</a>
</h3>
</div>
<div class="card-body">
<div id="budget-chart"></div>
</div>
</div>
</div>
</div>
<div class="row mb-2" x-data="budgets">
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title"><a href="{{ route('budgets.index') }}"
title="{{ __('firefly.go_to_budgets') }}">{{ __('firefly.budgetsAndSpending') }}</a>
</h3>
</div>
<div class="card-body">
<div id="budget-chart"></div>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title"><a href="{{ route('accounts.index',['asset']) }}"
title="{{ __('firefly.yourAccounts') }}">cat</a>
</h3>
</div>
<div class="card-body">
<div id="category-chart"></div>
</div>
</div>
</div>
</div>
<div class="row" x-data="categories">
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title"><a href="{{ route('categories.index') }}"
title="{{ __('firefly.yourAccounts') }}">{{ __('firefly.categories') }}</a>
</h3>
</div>
<div class="card-body">
<div id="category-chart"></div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-12 col-sm-12 col-xs-12">
<div class="row">
<template x-for="account in accountList">
<div class="col-12 mb-2" x-model="account">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<a :href="'{{ route('accounts.show','') }}/' + account.id"
x-text="account.name"></a>
</div>
<div class="col-xl-4 col-lg-12 col-sm-12 col-xs-12">
<div class="row">
<template x-for="account in accountList">
<div class="col-12 mb-2" x-model="account">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<a :href="'{{ route('accounts.show','') }}/' + account.id"
x-text="account.name"></a>
<span class="small text-muted">(<span
x-text="account.balance"></span>)</span>
</h3>
</div>
<div class="card-body p-0">
<p class="text-center small" x-show="account.groups.length < 1">
TODO No transactions
<span class="small text-muted">(<span
x-text="account.balance"></span>)</span>
</h3>
</div>
<div class="card-body p-0">
<p class="text-center small" x-show="account.groups.length < 1">
TODO No transactions
</p>
<table class="table table-sm" x-show="account.groups.length > 0">
<tbody>
<template x-for="group in account.groups">
<tr>
<td>
<template x-if="group.title">
</p>
<table class="table table-sm" x-show="account.groups.length > 0">
<tbody>
<template x-for="group in account.groups">
<tr>
<td>
<template x-if="group.title">
<span><a
:href="'{{route('transactions.show', '') }}/' + group.id"
x-text="group.title"></a><br/></span>
</template>
<template x-for="transaction in group.transactions">
</template>
<template x-for="transaction in group.transactions">
<span>
<template x-if="group.title">
<span>-
@@ -119,32 +117,74 @@
</span>
</template>
</span>
</template>
</td>
<td style="width:30%;" class="text-end">
<template x-if="group.title">
<span><br/></span>
</template>
<template x-for="transaction in group.transactions">
</template>
</td>
<td style="width:30%;" class="text-end">
<template x-if="group.title">
<span><br/></span>
</template>
<template x-for="transaction in group.transactions">
<span>
<span x-text="transaction.amount"></span><br>
</span>
</template>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
</div>
</template>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title"><a href="#" title="Something">Expense accounts</a></h3>
</div>
<div class="card-body">
</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title"><a href="#" title="Something">Bills</a></h3>
</div>
<div class="card-body">
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title"><a href="#" title="Something">Spaarpotjes</a></h3>
</div>
<div class="card-body">
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title"><a href="#" title="Something">Revenue</a></h3>
</div>
<div class="card-body">
</div>
</div>
</div>
</div>
<!-- row with budget chart -->
</div>