mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-02-25 18:45:27 -06:00
Budget and category charts in new layout.
This commit is contained in:
@@ -194,6 +194,8 @@ class AccountController extends Controller
|
||||
'currency_code' => $currency->code,
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_decimal_places' => $currency->decimal_places,
|
||||
'start_date' => $start->format('Y-m-d'),
|
||||
'end_date' => $end->format('Y-m-d'),
|
||||
'type' => 'line', // line, area or bar
|
||||
'yAxisID' => 0, // 0, 1, 2
|
||||
'entries' => [],
|
||||
|
||||
308
app/Api/V1/Controllers/Chart/BudgetController.php
Normal file
308
app/Api/V1/Controllers/Chart/BudgetController.php
Normal file
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
/**
|
||||
* BudgetController.php
|
||||
* Copyright (c) 2020 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/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Api\V1\Controllers\Chart;
|
||||
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Api\V1\Controllers\Controller;
|
||||
use FireflyIII\Api\V1\Requests\DateRequest;
|
||||
use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\BudgetLimit;
|
||||
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\Budget\OperationsRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class BudgetController
|
||||
*/
|
||||
class BudgetController extends Controller
|
||||
{
|
||||
private BudgetLimitRepositoryInterface $blRepository;
|
||||
private OperationsRepositoryInterface $opsRepository;
|
||||
private BudgetRepositoryInterface $repository;
|
||||
|
||||
/**
|
||||
* BudgetController constructor.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
//$this->generator = app(GeneratorInterface::class);
|
||||
$this->repository = app(BudgetRepositoryInterface::class);
|
||||
$this->opsRepository = app(OperationsRepositoryInterface::class);
|
||||
$this->blRepository = app(BudgetLimitRepositoryInterface::class);
|
||||
|
||||
//$this->nbRepository = app(NoBudgetRepositoryInterface::class);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* [
|
||||
* 'label' => 'label for entire set'
|
||||
* 'currency_id' => 0,
|
||||
* 'currency_code' => 'EUR',
|
||||
* 'currency_symbol' => '$',
|
||||
* 'currency_decimal_places' => 2,
|
||||
* 'type' => 'bar', // line, area or bar
|
||||
* 'yAxisID' => 0, // 0, 1, 2
|
||||
* 'entries' => ['a' => 1, 'b' => 4],
|
||||
* ],
|
||||
*
|
||||
* @param DateRequest $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function overview(DateRequest $request): JsonResponse
|
||||
{
|
||||
$dates = $request->getAll();
|
||||
$budgets = $this->repository->getActiveBudgets();
|
||||
$budgetNames = [];
|
||||
$currencyNames = [];
|
||||
$sets = [];
|
||||
/** @var Budget $budget */
|
||||
foreach ($budgets as $budget) {
|
||||
$expenses = $this->getExpenses($budget, $dates['start'], $dates['end']);
|
||||
$expenses = $this->filterNulls($expenses);
|
||||
foreach ($expenses as $set) {
|
||||
$budgetNames[] = $set['budget_name'];
|
||||
$currencyNames[] = $set['currency_name'];
|
||||
$sets[] = $set;
|
||||
}
|
||||
}
|
||||
$budgetNames = array_unique($budgetNames);
|
||||
$currencyNames = array_unique($currencyNames);
|
||||
$basic = $this->createSets($budgetNames, $currencyNames);
|
||||
$filled = $this->fillSets($basic, $sets);
|
||||
$keys = array_values($filled);
|
||||
|
||||
return response()->json($keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $limits
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getExpenses(Budget $budget, Carbon $start, Carbon $end): array
|
||||
{
|
||||
$limits = $this->blRepository->getBudgetLimits($budget, $start, $end);
|
||||
if (0 === $limits->count()) {
|
||||
return $this->getExpenseInRange($budget, $start, $end);
|
||||
}
|
||||
$arr = [];
|
||||
/** @var BudgetLimit $limit */
|
||||
foreach ($limits as $limit) {
|
||||
$arr[] = $this->getExpensesForLimit($limit);
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $budgetNames
|
||||
* @param array $currencyNames
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function createSets(array $budgetNames, array $currencyNames): array
|
||||
{
|
||||
$return = [];
|
||||
foreach ($currencyNames as $currencyName) {
|
||||
$entries = [];
|
||||
foreach ($budgetNames as $budgetName) {
|
||||
$label = sprintf('%s (%s)', $budgetName, $currencyName);
|
||||
$entries[$label] = '0';
|
||||
}
|
||||
|
||||
// left
|
||||
$return['left'] = [
|
||||
'label' => sprintf('%s (%s)', trans('firefly.left'), $currencyName),
|
||||
'data_type' => 'left',
|
||||
'currency_name' => $currencyName,
|
||||
'type' => 'bar',
|
||||
'yAxisID' => 0, // 0, 1, 2
|
||||
'entries' => $entries,
|
||||
];
|
||||
// // spent
|
||||
// $return['spent'] = [
|
||||
// 'label' => sprintf('%s (%s)', trans('firefly.spent'), $currencyName),
|
||||
// 'data_type' => 'spent',
|
||||
// 'currency_name' => $currencyName,
|
||||
// 'type' => 'bar',
|
||||
// 'yAxisID' => 0, // 0, 1, 2
|
||||
// 'entries' => $entries,
|
||||
// ];
|
||||
|
||||
|
||||
// spent_capped
|
||||
$return['spent_capped'] = [
|
||||
'label' => sprintf('%s (%s)', trans('firefly.spent'), $currencyName),
|
||||
'data_type' => 'spent_capped',
|
||||
'currency_name' => $currencyName,
|
||||
'type' => 'bar',
|
||||
'yAxisID' => 0, // 0, 1, 2
|
||||
'entries' => $entries,
|
||||
];
|
||||
|
||||
// overspent
|
||||
$return['overspent'] = [
|
||||
'label' => sprintf('%s (%s)', trans('firefly.overspent'), $currencyName),
|
||||
'data_type' => 'overspent',
|
||||
'currency_name' => $currencyName,
|
||||
'type' => 'bar',
|
||||
'yAxisID' => 0, // 0, 1, 2
|
||||
'entries' => $entries,
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $basic
|
||||
* @param array $sets
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function fillSets(array $basic, array $sets): array
|
||||
{
|
||||
foreach ($sets as $set) {
|
||||
$label = $set['label'];
|
||||
//$basic['spent']['entries'][$label] = $set['entries']['spent'];
|
||||
$basic['spent_capped']['entries'][$label] = $set['entries']['spent_capped'];
|
||||
$basic['left']['entries'][$label] = $set['entries']['left'];
|
||||
$basic['overspent']['entries'][$label] = $set['entries']['overspent'];
|
||||
}
|
||||
|
||||
return $basic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $expenses
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function filterNulls(array $expenses): array
|
||||
{
|
||||
$return = [];
|
||||
/** @var array|null $arr */
|
||||
foreach ($expenses as $arr) {
|
||||
if (null !== $arr) {
|
||||
$return[] = $arr;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Budget $budget
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getExpenseInRange(Budget $budget, Carbon $start, Carbon $end): array
|
||||
{
|
||||
$spent = $this->opsRepository->sumExpenses($start, $end, null, new Collection([$budget]), null);
|
||||
$return = [];
|
||||
/** @var array $set */
|
||||
foreach ($spent as $set) {
|
||||
$current = [
|
||||
'label' => sprintf('%s (%s)', $budget->name, $set['currency_name']),
|
||||
'budget_name' => $budget->name,
|
||||
'start_date' => $start->format('Y-m-d'),
|
||||
'end_date' => $end->format('Y-m-d'),
|
||||
'currency_id' => (int) $set['currency_id'],
|
||||
'currency_code' => $set['currency_code'],
|
||||
'currency_name' => $set['currency_name'],
|
||||
'currency_symbol' => $set['currency_symbol'],
|
||||
'currency_decimal_places' => (int) $set['currency_decimal_places'],
|
||||
'type' => 'bar', // line, area or bar,
|
||||
'entries' => [],
|
||||
];
|
||||
$sumSpent = bcmul($set['sum'], '-1'); // spent
|
||||
$current['entries']['spent'] = $sumSpent;
|
||||
$current['entries']['amount'] = '0';
|
||||
$current['entries']['spent_capped'] = $sumSpent;
|
||||
$current['entries']['left'] = '0';
|
||||
$current['entries']['overspent'] = '0';
|
||||
$return[] = $current;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BudgetLimit $limit
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function getExpensesForLimit(BudgetLimit $limit): ?array
|
||||
{
|
||||
$budget = $limit->budget;
|
||||
$spent = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection([$budget]), $limit->transactionCurrency);
|
||||
$currency = $limit->transactionCurrency;
|
||||
// when limited to a currency, the count is always one. Or it's empty.
|
||||
$set = array_shift($spent);
|
||||
if (null === $set) {
|
||||
return null;
|
||||
}
|
||||
$return = [
|
||||
'label' => sprintf('%s (%s)', $budget->name, $set['currency_name']),
|
||||
'budget_name' => $budget->name,
|
||||
'start_date' => $limit->start_date->format('Y-m-d'),
|
||||
'end_date' => $limit->end_date->format('Y-m-d'),
|
||||
'currency_id' => (int) $currency->id,
|
||||
'currency_code' => $currency->code,
|
||||
'currency_name' => $currency->name,
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_decimal_places' => (int) $currency->decimal_places,
|
||||
'type' => 'bar', // line, area or bar,
|
||||
'entries' => [],
|
||||
];
|
||||
$sumSpent = bcmul($set['sum'], '-1'); // spent
|
||||
$return['entries']['spent'] = $sumSpent;
|
||||
$return['entries']['amount'] = $limit->amount;
|
||||
$return['entries']['spent_capped'] = 1 === bccomp($sumSpent, $limit->amount) ? $limit->amount : $sumSpent;
|
||||
$return['entries']['left'] = 1 === bccomp($limit->amount, $sumSpent) ? bcadd($set['sum'], $limit->amount) : '0'; // left
|
||||
$return['entries']['overspent'] = 1 === bccomp($limit->amount, $sumSpent) ? '0' : bcmul(bcadd($set['sum'], $limit->amount), '-1'); // overspent
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -74,8 +74,6 @@ class CategoryController extends Controller
|
||||
* @param DateRequest $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
*
|
||||
* TODO after 4.8,0, simplify
|
||||
*/
|
||||
public function overview(DateRequest $request): JsonResponse
|
||||
{
|
||||
@@ -89,32 +87,15 @@ class CategoryController extends Controller
|
||||
|
||||
$tempData = [];
|
||||
$spentWith = $this->opsRepository->listExpenses($start, $end);
|
||||
$earnedWith = $this->opsRepository->listIncome($start, $end);
|
||||
$spentWithout = $this->noCatRepository->listExpenses($start, $end);
|
||||
$earnedWithout = $this->noCatRepository->listIncome($start, $end);
|
||||
$categories = [];
|
||||
|
||||
|
||||
foreach ([$spentWith, $earnedWith, $spentWithout, $earnedWithout] as $set) {
|
||||
foreach ([$spentWith, $spentWithout,] as $set) {
|
||||
foreach ($set as $currency) {
|
||||
foreach ($currency['categories'] as $category) {
|
||||
$categories[] = $category['name'];
|
||||
$inKey = sprintf('%d-i', $currency['currency_id']);
|
||||
$outKey = sprintf('%d-e', $currency['currency_id']);
|
||||
// make data arrays if not yet present.
|
||||
$tempData[$inKey] = $tempData[$inKey] ?? [
|
||||
'currency_id' => $currency['currency_id'],
|
||||
'label' => (string) trans('firefly.box_earned_in_currency', ['currency' => $currency['currency_name']]),
|
||||
'currency_code' => $currency['currency_code'],
|
||||
'currency_symbol' => $currency['currency_symbol'],
|
||||
'currency_decimal_places' => $currency['currency_decimal_places'],
|
||||
'type' => 'bar', // line, area or bar
|
||||
'yAxisID' => 0, // 0, 1, 2
|
||||
'entries' => [
|
||||
// per category:
|
||||
// "category" => 5,
|
||||
],
|
||||
];
|
||||
$tempData[$outKey] = $tempData[$outKey] ?? [
|
||||
'currency_id' => $currency['currency_id'],
|
||||
'label' => (string) trans('firefly.box_spent_in_currency', ['currency' => $currency['currency_name']]),
|
||||
@@ -123,16 +104,12 @@ class CategoryController extends Controller
|
||||
'currency_decimal_places' => $currency['currency_decimal_places'],
|
||||
'type' => 'bar', // line, area or bar
|
||||
'yAxisID' => 0, // 0, 1, 2
|
||||
'entries' => [
|
||||
// per category:
|
||||
// "category" => 5,
|
||||
],
|
||||
'entries' => [],
|
||||
];
|
||||
|
||||
foreach ($category['transaction_journals'] as $journal) {
|
||||
// is it expense or income?
|
||||
$letter = -1 === bccomp($journal['amount'], '0') ? 'e' : 'i';
|
||||
$currentKey = sprintf('%d-%s', $currency['currency_id'], $letter);
|
||||
$currentKey = sprintf('%d-%s', $currency['currency_id'], 'e');
|
||||
$name = $category['name'];
|
||||
$tempData[$currentKey]['entries'][$name] = $tempData[$currentKey]['entries'][$name] ?? '0';
|
||||
$tempData[$currentKey]['entries'][$name] = bcadd($tempData[$currentKey]['entries'][$name], $journal['amount']);
|
||||
|
||||
@@ -43,8 +43,7 @@ class AccountController extends Controller
|
||||
{
|
||||
use AccountFilter;
|
||||
|
||||
/** @var array */
|
||||
private $validFields;
|
||||
private array $validFields;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* TransferController.php
|
||||
* Copyright (c) 2020 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/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Api\V1\Controllers\Search;
|
||||
|
||||
use FireflyIII\Api\V1\Controllers\Controller;
|
||||
use FireflyIII\Api\V1\Requests\Search\TransferRequest;
|
||||
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
|
||||
use FireflyIII\Support\Search\TransferSearch;
|
||||
use FireflyIII\Transformers\TransactionGroupTransformer;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use League\Fractal\Resource\Collection as FractalCollection;
|
||||
|
||||
/**
|
||||
* Class TransferController
|
||||
*/
|
||||
class TransferController extends Controller
|
||||
{
|
||||
/**
|
||||
* @param TransferRequest $request
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function search(TransferRequest $request)
|
||||
{
|
||||
// configure transfer search to search for a > b
|
||||
$search = app(TransferSearch::class);
|
||||
$search->setSource($request->get('source'));
|
||||
$search->setDestination($request->get('destination'));
|
||||
$search->setAmount($request->get('amount'));
|
||||
$search->setDescription($request->get('description'));
|
||||
$search->setDate($request->get('date'));
|
||||
|
||||
$left = $search->search();
|
||||
|
||||
// configure transfer search to search for b > a
|
||||
$search->setSource($request->get('destination'));
|
||||
$search->setDestination($request->get('source'));
|
||||
$search->setAmount($request->get('amount'));
|
||||
$search->setDescription($request->get('description'));
|
||||
$search->setDate($request->get('date'));
|
||||
|
||||
$right = $search->search();
|
||||
|
||||
// add parameters to URL:
|
||||
$this->parameters->set('source', $request->get('source'));
|
||||
$this->parameters->set('destination', $request->get('destination'));
|
||||
$this->parameters->set('amount', $request->get('amount'));
|
||||
$this->parameters->set('description', $request->get('description'));
|
||||
$this->parameters->set('date', $request->get('date'));
|
||||
|
||||
// get all journal ID's.
|
||||
$total = $left->merge($right)->unique('id')->pluck('id')->toArray();
|
||||
if (0 === count($total)) {
|
||||
// forces search to be empty.
|
||||
$total = [-1];
|
||||
}
|
||||
|
||||
// collector to return results.
|
||||
$pageSize = (int) app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data;
|
||||
$manager = $this->getManager();
|
||||
/** @var User $admin */
|
||||
$admin = auth()->user();
|
||||
|
||||
// use new group collector:
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
$collector
|
||||
->setUser($admin)
|
||||
// all info needed for the API:
|
||||
->withAPIInformation()
|
||||
// set page size:
|
||||
->setLimit($pageSize)
|
||||
// set page to retrieve
|
||||
->setPage(1)
|
||||
->setJournalIds($total);
|
||||
|
||||
$paginator = $collector->getPaginatedGroups();
|
||||
$paginator->setPath(route('api.v1.search.transfers') . $this->buildParams());
|
||||
$transactions = $paginator->getCollection();
|
||||
|
||||
/** @var TransactionGroupTransformer $transformer */
|
||||
$transformer = app(TransactionGroupTransformer::class);
|
||||
$transformer->setParameters($this->parameters);
|
||||
|
||||
$resource = new FractalCollection($transactions, $transformer, 'transactions');
|
||||
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
|
||||
|
||||
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
|
||||
}
|
||||
}
|
||||
@@ -412,7 +412,7 @@ class BudgetController extends Controller
|
||||
$cache->addProperty($end);
|
||||
$cache->addProperty('chart.budget.frontpage');
|
||||
if ($cache->has()) {
|
||||
// return response()->json($cache->get()); // @codeCoverageIgnore
|
||||
return response()->json($cache->get()); // @codeCoverageIgnore
|
||||
}
|
||||
$budgets = $this->repository->getActiveBudgets();
|
||||
$chartData = [
|
||||
|
||||
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
@@ -1030,9 +1030,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@popperjs/core": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.4.3.tgz",
|
||||
"integrity": "sha512-r0jArf9l4hXBJVgWlFiDj3rC+51G9d+AfycAp2YacHeKZnslGQblxGdRpbprZ2snXzYVjrSu0Tt0TZFQAILipg=="
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.4.4.tgz",
|
||||
"integrity": "sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg=="
|
||||
},
|
||||
"@types/glob": {
|
||||
"version": "7.1.2",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
"@popperjs/core": "^2.4.3",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"bootstrap": "^4.5.0",
|
||||
"chart.js": "^2.9.3",
|
||||
"icheck-bootstrap": "^3.0.1",
|
||||
|
||||
2
frontend/public/js/dashboard.js
vendored
2
frontend/public/js/dashboard.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
frontend/public/js/register.js
vendored
2
frontend/public/js/register.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
frontend/public/js/vendor.js
vendored
2
frontend/public/js/vendor.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -100,9 +100,11 @@
|
||||
},
|
||||
getLabels() {
|
||||
let firstSet = this.dataSet[0];
|
||||
for (const entryLabel in firstSet.entries) {
|
||||
if (firstSet.entries.hasOwnProperty(entryLabel)) {
|
||||
this.newDataSet.labels.push(entryLabel);
|
||||
if (typeof firstSet !== 'undefined') {
|
||||
for (const entryLabel in firstSet.entries) {
|
||||
if (firstSet.entries.hasOwnProperty(entryLabel)) {
|
||||
this.newDataSet.labels.push(entryLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -111,18 +113,20 @@
|
||||
if (this.dataSet.hasOwnProperty(setKey)) {
|
||||
let newSet = {};
|
||||
let oldSet = this.dataSet[setKey];
|
||||
newSet.label = oldSet.label;
|
||||
newSet.type = oldSet.type;
|
||||
newSet.currency_symbol = oldSet.currency_symbol;
|
||||
newSet.currency_code = oldSet.currency_code;
|
||||
newSet.yAxisID = oldSet.yAxisID;
|
||||
newSet.data = [];
|
||||
for (const entryLabel in oldSet.entries) {
|
||||
if (oldSet.entries.hasOwnProperty(entryLabel)) {
|
||||
newSet.data.push(oldSet.entries[entryLabel]);
|
||||
if (typeof oldSet !== 'undefined') {
|
||||
newSet.label = oldSet.label;
|
||||
newSet.type = oldSet.type;
|
||||
newSet.currency_symbol = oldSet.currency_symbol;
|
||||
newSet.currency_code = oldSet.currency_code;
|
||||
newSet.yAxisID = oldSet.yAxisID;
|
||||
newSet.data = [];
|
||||
for (const entryLabel in oldSet.entries) {
|
||||
if (oldSet.entries.hasOwnProperty(entryLabel)) {
|
||||
newSet.data.push(oldSet.entries[entryLabel]);
|
||||
}
|
||||
}
|
||||
this.newDataSet.datasets.push(newSet);
|
||||
}
|
||||
this.newDataSet.datasets.push(newSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
148
frontend/src/components/charts/DefaultBarOptions.vue
Normal file
148
frontend/src/components/charts/DefaultBarOptions.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<!--
|
||||
- DefaultLineOptions.vue
|
||||
- Copyright (c) 2020 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>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "DefaultBarOptions",
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Takes a string phrase and breaks it into separate phrases no bigger than 'maxwidth', breaks are made at complete words.
|
||||
* https://stackoverflow.com/questions/21409717/chart-js-and-long-labels
|
||||
*
|
||||
* @param str
|
||||
* @param maxwidth
|
||||
* @returns {Array}
|
||||
*/
|
||||
formatLabel(str, maxwidth) {
|
||||
var sections = [];
|
||||
str = String(str);
|
||||
var words = str.split(" ");
|
||||
var temp = "";
|
||||
|
||||
words.forEach(function (item, index) {
|
||||
if (temp.length > 0) {
|
||||
var concat = temp + ' ' + item;
|
||||
|
||||
if (concat.length > maxwidth) {
|
||||
sections.push(temp);
|
||||
temp = "";
|
||||
} else {
|
||||
if (index === (words.length - 1)) {
|
||||
sections.push(concat);
|
||||
return;
|
||||
} else {
|
||||
temp = concat;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (index === (words.length - 1)) {
|
||||
sections.push(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.length < maxwidth) {
|
||||
temp = item;
|
||||
} else {
|
||||
sections.push(item);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return sections;
|
||||
},
|
||||
getDefaultOptions() {
|
||||
console.log('getDefaultOptions()');
|
||||
return {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
elements: {
|
||||
line: {
|
||||
cubicInterpolationMode: 'monotone'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
stacked: true,
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
// break ticks when too long.
|
||||
callback: function (value, index, values) {
|
||||
//return this.formatLabel(value, 20);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
yAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
callback: function (tickValue) {
|
||||
"use strict";
|
||||
let currencyCode = this.chart.data.datasets[0].currency_code ? this.chart.data.datasets[0].currency_code : 'EUR';
|
||||
return new Intl.NumberFormat(window.localeValue, {style: 'currency', currency: currencyCode}).format(tickValue);
|
||||
},
|
||||
beginAtZero: true
|
||||
}
|
||||
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'label',
|
||||
callbacks: {
|
||||
label: function (tooltipItem, data) {
|
||||
"use strict";
|
||||
let currencyCode = data.datasets[tooltipItem.datasetIndex].currency_code ? data.datasets[tooltipItem.datasetIndex].currency_code : 'EUR';
|
||||
let nrString =
|
||||
new Intl.NumberFormat(window.localeValue, {style: 'currency', currency: currencyCode}).format(tooltipItem.yLabel)
|
||||
|
||||
return data.datasets[tooltipItem.datasetIndex].label + ': ' + nrString;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -30,10 +30,10 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
|
||||
<main-budget-chart/>
|
||||
<main-budget/>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
|
||||
<main-category-chart/>
|
||||
<main-category />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
<h3 class="card-title">{{ $t('firefly.yourAccounts') }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="main-account-chart">
|
||||
<main-account-chart v-if="loaded" :styles="myStyles" :options="chartOptions" :chart-data="chartData"></main-account-chart>
|
||||
<div>
|
||||
<main-account-chart v-if="loaded" :styles="chartStyles" :options="chartOptions" :chart-data="chartData"></main-account-chart>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
},
|
||||
computed: {
|
||||
myStyles() {
|
||||
chartStyles() {
|
||||
return {
|
||||
height: '400px',
|
||||
'max-height': '400px',
|
||||
@@ -79,8 +79,3 @@
|
||||
name: "MainAccount"
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.main-account-chart {
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
82
frontend/src/components/dashboard/MainBudget.vue
Normal file
82
frontend/src/components/dashboard/MainBudget.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<!--
|
||||
- MainBudgetChart.vue
|
||||
- Copyright (c) 2020 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>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{ $t('firefly.budgets') }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<main-budget-chart v-if="loaded" :styles="chartStyles" :options="chartOptions" :chart-data="chartData"></main-budget-chart>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="./budgets" class="btn btn-default button-sm"><i class="far fa-money-bill-alt"></i> {{ $t('firefly.go_to_budgets') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MainBudgetChart from "./MainBudgetChart";
|
||||
import DefaultBarOptions from "../charts/DefaultBarOptions";
|
||||
import DataConverter from "../charts/DataConverter";
|
||||
|
||||
export default {
|
||||
name: "MainBudget",
|
||||
components: {
|
||||
MainBudgetChart
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chartData: null,
|
||||
loaded: false,
|
||||
chartOptions: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.chartOptions = DefaultBarOptions.methods.getDefaultOptions();
|
||||
this.loaded = false;
|
||||
axios.get('./api/v1/chart/budget/overview?start=' + window.sessionStart + '&end=' + window.sessionEnd)
|
||||
.then(response => {
|
||||
this.chartData = response.data;
|
||||
//this.chartData = DataConverter.methods.colorizeData(this.chartData);
|
||||
this.chartData = DataConverter.methods.convertChart(this.chartData);
|
||||
this.loaded = true
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
computed: {
|
||||
chartStyles() {
|
||||
return {
|
||||
height: '400px',
|
||||
'max-height': '400px',
|
||||
position: 'relative',
|
||||
display: 'block',
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -18,25 +18,17 @@
|
||||
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">I am a card</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
I am card body
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {Line} from 'vue-chartjs'
|
||||
|
||||
export default {
|
||||
name: "MainBudgetChart"
|
||||
name: "MainBudgetChart",
|
||||
extends: Line,
|
||||
props: ['options', 'chartData'],
|
||||
|
||||
mounted() {
|
||||
this.renderChart(this.chartData, this.options)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
81
frontend/src/components/dashboard/MainCategory.vue
Normal file
81
frontend/src/components/dashboard/MainCategory.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<!--
|
||||
- MainCategoryChart.vue
|
||||
- Copyright (c) 2020 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>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{ $t('firefly.categories') }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<main-category-chart v-if="loaded" :styles="chartStyles" :options="chartOptions" :chart-data="chartData"></main-category-chart>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="./categories" class="btn btn-default button-sm"><i class="far fa-money-bill-alt"></i> {{ $t('firefly.go_to_categories') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MainCategoryChart from "./MainCategoryChart";
|
||||
import DefaultBarOptions from "../charts/DefaultBarOptions";
|
||||
import DataConverter from "../charts/DataConverter";
|
||||
|
||||
export default {
|
||||
name: "MainCategory",
|
||||
components: {
|
||||
MainCategoryChart
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chartData: null,
|
||||
loaded: false,
|
||||
chartOptions: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.chartOptions = DefaultBarOptions.methods.getDefaultOptions();
|
||||
this.loaded = false;
|
||||
axios.get('./api/v1/chart/category/overview?start=' + window.sessionStart + '&end=' + window.sessionEnd)
|
||||
.then(response => {
|
||||
this.chartData = response.data;
|
||||
this.chartData = DataConverter.methods.convertChart(this.chartData);
|
||||
this.loaded = true
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
computed: {
|
||||
chartStyles() {
|
||||
return {
|
||||
height: '400px',
|
||||
'max-height': '400px',
|
||||
position: 'relative',
|
||||
display: 'block',
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -17,23 +17,17 @@
|
||||
- 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>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">I am a card</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
I am card body
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {Line} from "vue-chartjs";
|
||||
|
||||
export default {
|
||||
name: "MainCategoryChart"
|
||||
name: "MainCategoryChart",
|
||||
extends: Line,
|
||||
props: ['options', 'chartData'],
|
||||
|
||||
mounted() {
|
||||
this.renderChart(this.chartData, this.options)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -64,10 +64,14 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
cat
|
||||
<span v-for="tr in transaction.attributes.transactions">
|
||||
<a :href="'categories/show/' + transaction.category_id" v-if="0!==tr.category_id">{{ tr.category_name }}</a><br />
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
bud
|
||||
<span v-for="tr in transaction.attributes.transactions">
|
||||
<a :href="'budgets/show/' + transaction.budget_id" v-if="0!==tr.budget_id">{{ tr.budget_name }}</a><br />
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
8
frontend/src/pages/dashboard.js
vendored
8
frontend/src/pages/dashboard.js
vendored
@@ -23,8 +23,8 @@ import TopBoxes from "../components/dashboard/TopBoxes";
|
||||
import MainAccount from "../components/dashboard/MainAccount";
|
||||
import MainAccountList from "../components/dashboard/MainAccountList";
|
||||
import MainBillsChart from "../components/dashboard/MainBillsChart";
|
||||
import MainBudgetChart from "../components/dashboard/MainBudgetChart";
|
||||
import MainCategoryChart from "../components/dashboard/MainCategoryChart";
|
||||
import MainBudget from "../components/dashboard/MainBudget";
|
||||
import MainCategory from "../components/dashboard/MainCategory";
|
||||
import MainCrebitChart from "../components/dashboard/MainCrebitChart";
|
||||
import MainDebitChart from "../components/dashboard/MainDebitChart";
|
||||
import MainPiggyList from "../components/dashboard/MainPiggyList";
|
||||
@@ -49,8 +49,8 @@ Vue.component('top-boxes', TopBoxes);
|
||||
Vue.component('main-account', MainAccount);
|
||||
Vue.component('main-account-list', MainAccountList);
|
||||
Vue.component('main-bills-chart', MainBillsChart);
|
||||
Vue.component('main-budget-chart', MainBudgetChart);
|
||||
Vue.component('main-category-chart', MainCategoryChart);
|
||||
Vue.component('main-budget', MainBudget);
|
||||
Vue.component('main-category', MainCategory);
|
||||
Vue.component('main-credit-chart', MainCrebitChart);
|
||||
Vue.component('main-debit-chart', MainDebitChart);
|
||||
Vue.component('main-piggy-list', MainPiggyList);
|
||||
|
||||
2
public/v2/js/dashboard.js
vendored
2
public/v2/js/dashboard.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/v2/js/register.js
vendored
2
public/v2/js/register.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/v2/js/vendor.js
vendored
2
public/v2/js/vendor.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1319,6 +1319,7 @@ return [
|
||||
'month' => 'Month',
|
||||
'budget' => 'Budget',
|
||||
'spent' => 'Spent',
|
||||
'spent_capped' => 'Spent (capped)',
|
||||
'spent_in_budget' => 'Spent in budget',
|
||||
'left_to_spend' => 'Left to spend',
|
||||
'earned' => 'Earned',
|
||||
|
||||
@@ -197,6 +197,17 @@ Route::group(
|
||||
}
|
||||
);
|
||||
|
||||
// Budgets
|
||||
Route::group(
|
||||
['namespace' => 'FireflyIII\Api\V1\Controllers\Chart', 'prefix' => 'chart/budget',
|
||||
'as' => 'api.v1.chart.budget.',],
|
||||
static function () {
|
||||
|
||||
// (frontpage) budget overview
|
||||
Route::get('overview', ['uses' => 'BudgetController@overview', 'as' => 'overview']);
|
||||
}
|
||||
);
|
||||
|
||||
// Categories
|
||||
Route::group(
|
||||
['namespace' => 'FireflyIII\Api\V1\Controllers\Chart', 'prefix' => 'chart/category',
|
||||
@@ -349,7 +360,6 @@ Route::group(
|
||||
// Attachment API routes:
|
||||
Route::get('transactions', ['uses' => 'TransactionController@search', 'as' => 'transactions']);
|
||||
Route::get('accounts', ['uses' => 'AccountController@search', 'as' => 'accounts']);
|
||||
Route::get('transfers', ['uses' => 'TransferController@search', 'as' => 'transfers']);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user