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,68 @@
<?php
declare(strict_types=1);
/*
* BudgetController.php
* 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/>.
*/
namespace FireflyIII\Api\V2\Controllers\Chart;
use Carbon\Carbon;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Generic\DateRequest;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Administration\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Administration\Budget\OperationsRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
/**
* Class BudgetController
*/
class CategoryController extends Controller
{
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
return $next($request);
}
);
}
/**
* @param DateRequest $request
*
* @return JsonResponse
* @throws FireflyException
*/
public function dashboard(DateRequest $request): JsonResponse {}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,35 +1,35 @@
{
"node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf": {
"file": "assets/fa-brands-400-20c4a58b.ttf",
"file": "assets/fa-brands-400-003f1154.ttf",
"src": "node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf"
},
"node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2": {
"file": "assets/fa-brands-400-74833209.woff2",
"file": "assets/fa-brands-400-faae6fc0.woff2",
"src": "node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2"
},
"node_modules/@fortawesome/fontawesome-free/webfonts/fa-regular-400.ttf": {
"file": "assets/fa-regular-400-528d022d.ttf",
"file": "assets/fa-regular-400-7d81a1a7.ttf",
"src": "node_modules/@fortawesome/fontawesome-free/webfonts/fa-regular-400.ttf"
},
"node_modules/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff2": {
"file": "assets/fa-regular-400-8e7e5ea1.woff2",
"file": "assets/fa-regular-400-9169d8be.woff2",
"src": "node_modules/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff2"
},
"node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.ttf": {
"file": "assets/fa-solid-900-67a65763.ttf",
"file": "assets/fa-solid-900-cea79b34.ttf",
"src": "node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.ttf"
},
"node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff2": {
"file": "assets/fa-solid-900-7152a693.woff2",
"file": "assets/fa-solid-900-886c8611.woff2",
"src": "node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff2"
},
"resources/assets/v2/dashboard.js": {
"file": "assets/dashboard-157793a8.js",
"file": "assets/dashboard-3896887c.js",
"isEntry": true,
"src": "resources/assets/v2/dashboard.js"
},
"resources/assets/v2/sass/app.scss": {
"file": "assets/app-40e01f65.css",
"file": "assets/app-28a195fd.css",
"isEntry": true,
"src": "resources/assets/v2/sass/app.scss"
}

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,32 +37,30 @@ 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)
this.chartData = response.data;
this.generateOptions(this.chartData);
this.drawChart();
});
},
generateOptions(data) {
window.currencies = [];
let options = {
legend: {show: false},
chart: {
height: 400,
toolbar: {tools: {zoom: false, download: false, pan: false}},
type: 'line'
}, series: [],
},
series: [],
settings: [],
xaxis: {
categories: [],
@@ -79,7 +77,8 @@ export default () => ({
return ':(';
}
}
}, yaxis: {
},
yaxis: {
labels: {
formatter: function (value, index) {
if (undefined === value) {
@@ -99,9 +98,9 @@ export default () => ({
},
};
// render data:
for (let i = 0; i < response.data.length; i++) {
if (response.data.hasOwnProperty(i)) {
let current = response.data[i];
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
@@ -120,23 +119,37 @@ export default () => ({
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;
});
}, loadAccounts() {
console.log('loadAccounts');
if (true === this.loadingAccounts) {
console.log('loadAccounts CANCELLED');
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,19 +31,21 @@ 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.boxData = response.data;
this.generateOptions(this.boxData);
//this.drawChart();
});
},
generateOptions(data) {
this.balanceBox = {amounts: [], subtitles: []};
this.billBox = {paid: [], unpaid: []};
this.leftBox = {left: [], perDay: []};
@@ -51,9 +53,9 @@ export default () => ({
let subtitles = {};
// process new content:
for (const i in response.data) {
if (response.data.hasOwnProperty(i)) {
const current = response.data[i];
for (const i in data) {
if (data.hasOwnProperty(i)) {
const current = data[i];
let key = current.key;
// native (auto conversion):
if (this.autoConversion) {
@@ -171,9 +173,22 @@ export default () => ({
this.balanceBox.subtitles.push(subtitles[i]);
}
}
this.loading = false;
});
},
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,18 +24,46 @@ 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) => {
this.chartData = response.data;
this.generateOptions(this.chartData);
this.drawChart();
});
},
generateOptions(data) {
window.budgetCurrencies = [];
let options = {
legend: {show: false},
series: [{
@@ -74,10 +102,10 @@ export default () => ({
dataLabels: {
total: {
enabled: true,
style: {
fontSize: '13px',
fontWeight: 900
},
// style: {
// fontSize: '13px',
// fontWeight: 900
// },
formatter: function (val, opt) {
let index = 0;
if (typeof opt === 'object') {
@@ -93,7 +121,6 @@ export default () => ({
yaxis: {
labels: {
formatter: function (value, index) {
if (undefined === value) {
return value;
}
@@ -127,9 +154,9 @@ export default () => ({
};
for (const i in response.data) {
if (response.data.hasOwnProperty(i)) {
let current = response.data[i];
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);
@@ -155,27 +182,18 @@ export default () => ({
}
}
if (null !== this.chart) {
// chart already in place, refresh:
this.chart.updateOptions(options);
}
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,8 +9,7 @@
<div class="container-fluid">
@include('partials.dashboard.boxes')
<!-- row with account data -->
<div>
<div class="row" x-data="accounts">
<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">
@@ -49,7 +48,7 @@
<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>
title="{{ __('firefly.go_to_budgets') }}">{{ __('firefly.budgetsAndSpending') }}</a>
</h3>
</div>
<div class="card-body">
@@ -59,19 +58,18 @@
</div>
</div>
<div class="row mb-2">
<div class="row" x-data="categories">
<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 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>
@@ -143,8 +141,50 @@
</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>