This commit is contained in:
James Cole 2022-09-18 16:28:04 +02:00
parent c120a908f3
commit c25b63d87b
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
10 changed files with 71 additions and 437 deletions

View File

@ -1,267 +0,0 @@
<?php
/**
* AvailableBudgetController.php
* Copyright (c) 2019 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\Http\Controllers\Budget;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidDateException;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\AvailableBudget;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Budget\AvailableBudgetRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Illuminate\View\View;
use Log;
use ValueError;
/**
*
* Class AvailableBudgetController
*/
class AvailableBudgetController extends Controller
{
/** @var AvailableBudgetRepositoryInterface */
private $abRepository;
/** @var CurrencyRepositoryInterface */
private $currencyRepos;
/**
* AmountController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
app('view')->share('title', (string) trans('firefly.budgets'));
app('view')->share('mainTitleIcon', 'fa-pie-chart');
$this->abRepository = app(AvailableBudgetRepositoryInterface::class);
$this->currencyRepos = app(CurrencyRepositoryInterface::class);
return $next($request);
}
);
}
/**
* Create will always assume the user's default currency, if it's not set.
*
* This method will check if there is no AB, and refuse to continue if it exists.
*
* @param Request $request
* @param Carbon $start
* @param Carbon $end
* @param TransactionCurrency|null $currency
*
* @return Factory|RedirectResponse|Redirector|View
*/
public function create(Request $request, Carbon $start, Carbon $end, ?TransactionCurrency $currency = null)
{
$currency = $currency ?? app('amount')->getDefaultCurrency();
$collection = $this->abRepository->get($start, $end);
$filtered = $collection->filter(
static function (AvailableBudget $budget) use ($currency) {
return $currency->id === $budget->transaction_currency_id;
}
);
if ($filtered->count() > 0) {
/** @var AvailableBudget $first */
$first = $filtered->first();
return redirect(route('available-budgets.edit', [$first->id]));
}
$page = (int) ($request->get('page') ?? 1);
return view('budgets.available-budgets.create', compact('start', 'end', 'page', 'currency'));
}
/**
* createAlternative will show a list of enabled currencies so the user can pick one.
*
* @param Request $request
* @param Carbon $start
* @param Carbon $end
*
* @return Factory|View
*/
public function createAlternative(Request $request, Carbon $start, Carbon $end)
{
$currencies = $this->currencyRepos->get();
$availableBudgets = $this->abRepository->get($start, $end);
// remove already budgeted currencies:
$currencies = $currencies->filter(
static function (TransactionCurrency $currency) use ($availableBudgets) {
/** @var AvailableBudget $budget */
foreach ($availableBudgets as $budget) {
if ($budget->transaction_currency_id === $currency->id) {
return false;
}
}
return true;
}
);
$page = (int) ($request->get('page') ?? 1);
return view('budgets.available-budgets.create-alternative', compact('start', 'end', 'page', 'currencies'));
}
/**
* @param Request $request
*
* @return RedirectResponse|Redirector
*/
public function delete(Request $request)
{
$id = (int) $request->get('id');
if (0 !== $id) {
$availableBudget = $this->abRepository->findById($id);
if (null !== $availableBudget) {
$this->abRepository->destroyAvailableBudget($availableBudget);
session()->flash('success', trans('firefly.deleted_ab'));
}
}
return redirect(route('budgets.index'));
}
/**
* @param AvailableBudget $availableBudget
*
* @param Carbon $start
* @param Carbon $end
*
* @return Factory|View
*/
public function edit(AvailableBudget $availableBudget, Carbon $start, Carbon $end)
{
$availableBudget->amount = number_format((float) $availableBudget->amount, $availableBudget->transactionCurrency->decimal_places, '.', '');
return view('budgets.available-budgets.edit', compact('availableBudget', 'start', 'end'));
}
/**
* @param Request $request
*
* @return RedirectResponse|Redirector
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
public function store(Request $request)
{
// make dates.
try {
$start = Carbon::createFromFormat('Y-m-d', $request->get('start'));
$end = Carbon::createFromFormat('Y-m-d', $request->get('end'));
} catch (InvalidDateException $e) {
$start = session()->get('start');
$end = session()->get('end');
Log::info($e->getMessage());
}
// validate amount
$amount = (string) $request->get('amount');
if ('' === $amount) {
session()->flash('error', trans('firefly.invalid_amount'));
return redirect(route('budgets.index', [$start->format('Y-m-d'), $end->format('Y-m-d')]));
}
if (bccomp($amount, '0') <= 0) {
session()->flash('error', trans('firefly.invalid_amount'));
return redirect(route('budgets.index', [$start->format('Y-m-d'), $end->format('Y-m-d')]));
}
// find currency
$currency = $this->currencyRepos->find((int) $request->get('currency_id'));
if (null === $currency) {
session()->flash('error', trans('firefly.invalid_currency'));
return redirect(route('budgets.index', [$start->format('Y-m-d'), $end->format('Y-m-d')]));
}
$start->startOfDay();
$end->endOfDay();
// find existing AB
$existing = $this->abRepository->find($currency, $start, $end);
if (null === $existing) {
$this->abRepository->store(
[
'amount' => $amount,
'currency_id' => $currency->id,
'start' => $start,
'end' => $end,
]
);
}
if (null !== $existing) {
// update amount:
$this->abRepository->update($existing, ['amount' => $amount]);
}
session()->flash('success', trans('firefly.set_ab'));
return redirect(route('budgets.index', [$start->format('Y-m-d'), $end->format('Y-m-d')]));
}
/**
* @param Request $request
* @param AvailableBudget $availableBudget
*
* @param Carbon $start
* @param Carbon $end
*
* @return RedirectResponse|Redirector
*/
public function update(Request $request, AvailableBudget $availableBudget, Carbon $start, Carbon $end)
{
// validate amount
$amount = (string) $request->get('amount');
if ('' === $amount) {
session()->flash('error', trans('firefly.invalid_amount'));
return redirect(route('budgets.index', [$start->format('Y-m-d'), $end->format('Y-m-d')]));
}
try {
if (bccomp($amount, '0') <= 0) {
session()->flash('error', trans('firefly.invalid_amount'));
return redirect(route('budgets.index', [$start->format('Y-m-d'), $end->format('Y-m-d')]));
}
} catch (ValueError $e) {
Log::error(sprintf('Value "%s" is not a number: %s', $amount, $e->getMessage()));
session()->flash('error', trans('firefly.invalid_amount'));
return redirect(route('budgets.index', [$start->format('Y-m-d'), $end->format('Y-m-d')]));
}
$this->abRepository->update($availableBudget, ['amount' => $amount]);
session()->flash('success', trans('firefly.updated_ab'));
return redirect(route('budgets.index', [$start->format('Y-m-d'), $end->format('Y-m-d')]));
}
}

View File

@ -129,9 +129,6 @@ class IndexController extends Controller
unset($spentArr);
}
// count the number of enabled currencies. This determines if we display a "+" button.
$enableAddButton = $currencies->count() > count($availableBudgets);
// number of days for consistent budgeting.
$activeDaysPassed = $this->activeDaysPassed($start, $end); // see method description.
$activeDaysLeft = $this->activeDaysLeft($start, $end); // see method description.
@ -141,7 +138,7 @@ class IndexController extends Controller
return view(
'budgets.index', compact(
'availableBudgets', 'budgeted', 'spent', 'prevLoop', 'nextLoop', 'budgets', 'currencies', 'enableAddButton', 'periodTitle',
'availableBudgets', 'budgeted', 'spent', 'prevLoop', 'nextLoop', 'budgets', 'currencies', 'periodTitle',
'defaultCurrency', 'activeDaysPassed', 'activeDaysLeft', 'inactive', 'budgets', 'start', 'end', 'sums'
)
);

View File

@ -102,8 +102,8 @@ class BudgetController extends Controller
[
'budgeted' => $budgeted,
'budgeted_formatted' => app('amount')->formatAnything($currency, $budgeted, true),
'available' => app('amount')->formatAnything($currency, $available, true),
'available_formatted' => $available,
'available' => $available,
'available_formatted' => app('amount')->formatAnything($currency, $available, true),
'percentage' => $percentage,
'currency_id' => $currency->id,
'currency_code' => $currency->code,

View File

@ -39,8 +39,11 @@ use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Events\UserChangedEmail;
use FireflyIII\Events\WarnUserAboutBill;
use FireflyIII\Mail\OAuthTokenCreatedMail;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\PiggyBankRepetition;
use FireflyIII\Repositories\Budget\AvailableBudgetRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use Illuminate\Auth\Events\Login;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@ -150,6 +153,54 @@ class EventServiceProvider extends ServiceProvider
{
parent::boot();
$this->registerCreateEvents();
$this->registerBudgetEvents();
}
/**
*
*/
protected function registerBudgetEvents(): void
{
$func = static function (BudgetLimit $limit) {
Log::debug('Trigger budget limit event.');
// find available budget with same period and same currency or create it.
// then set it or add money:
$user = $limit->budget->user;
$availableBudget = $user
->availableBudgets()
->where('start_date', $limit->start_date->format('Y-m-d'))
->where('end_date', $limit->end_date->format('Y-m-d'))
->where('transaction_currency_id', $limit->transaction_currency_id)
->first();
// update!
if (null !== $availableBudget) {
$repository = app(BudgetLimitRepositoryInterface::class);
$repository->setUser($user);
$set = $repository->getAllBudgetLimitsByCurrency($limit->transactionCurrency, $limit->start_date, $limit->end_date);
$sum = (string) $set->sum('amount');
Log::debug(sprintf('Because budget limit #%d had its amount changed to %s, available budget limit #%d will be updated.', $limit->id, $limit->amount, $availableBudget->id));
$availableBudget->amount = $sum;
$availableBudget->save();
return;
}
Log::debug('Does not exist, create it.');
// create it.
$data = [
'amount' => $limit->amount,
'start' => $limit->start_date,
'end' => $limit->end_date,
'currency_id' => $limit->transaction_currency_id,
];
$repository = app(AvailableBudgetRepositoryInterface::class);
$repository->setUser($user);
$repository->store($data);
};
BudgetLimit::created($func);
BudgetLimit::updated($func);
}
/**
@ -157,6 +208,8 @@ class EventServiceProvider extends ServiceProvider
*/
protected function registerCreateEvents(): void
{
// in case of repeated piggy banks and/or other problems.
PiggyBank::created(
static function (PiggyBank $piggyBank) {

View File

@ -29,9 +29,9 @@ $(function () {
drawSpentBars();
drawBudgetedBars();
$('.update_ab').on('click', updateAvailableBudget);
$('.delete_ab').on('click', deleteAvailableBudget);
$('.create_ab_alt').on('click', createAltAvailableBudget);
//$('.update_ab').on('click', updateAvailableBudget);
//$('.delete_ab').on('click', deleteAvailableBudget);
//$('.create_ab_alt').on('click', createAltAvailableBudget);
$('.budget_amount').on('change', updateBudgetedAmount);
$('.create_bl').on('click', createBudgetLimit);
@ -127,20 +127,28 @@ function updateBudgetedAmount(e) {
}
function updateTotalBudgetedAmount(currencyId) {
console.log('updateTotalBudgetedAmount');
// fade info away:
$('span.budgeted_amount[data-currency="' + currencyId + '"]')
.fadeTo(100, 0.1, function () {
//$(this).fadeTo(500, 1.0);
});
$('span.available_amount[data-currency="' + currencyId + '"]')
.fadeTo(100, 0.1, function () {
});
// get new amount:
$.get(totalBudgetedUrl.replace('REPLACEME', currencyId)).done(function (data) {
// set thing:
$('span.budgeted_amount[data-currency="' + currencyId + '"]')
.html(data.budgeted_formatted)
// fade back:
.fadeTo(300, 1.0);
// also set available amount:
$('span.available_amount[data-currency="' + currencyId + '"]')
.html(data.available_formatted).fadeTo(300, 1.0);
// set bar:
var pct = parseFloat(data.percentage);
if (pct <= 100) {
@ -214,7 +222,7 @@ function deleteBudgetLimit(e) {
var url = deleteBudgetLimitUrl.replace('REPLACEME', budgetLimitId.toString());
$.post(url, {_token: token}).then(function () {
$('.bl_entry[data-budget-limit-id="' + budgetLimitId + '"]').remove();
});
return false;
}

View File

@ -1,37 +0,0 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span>&times;</span><span class="sr-only">{{ 'close'|_ }}</span>
</button>
<h4 class="modal-title">
{{ trans('firefly.update_budget_amount_range',
{start: start.isoFormat(monthAndDayFormat), end: end.isoFormat(monthAndDayFormat)}) }}
</h4>
</div>
<form style="display: inline;" id="income" action="{{ route('available-budgets.store') }}" method="POST">
<div class="modal-body">
<input type="hidden" name="_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="start" value="{{ start.format('Y-m-d') }}"/>
<input type="hidden" name="end" value="{{ end.format('Y-m-d') }}"/>
<input type="hidden" name="page" value="{{ page }}"/>
<div class="form-group">
<select class="form-control" name="currency_id">
{% for currency in currencies %}
<option label="{{ currency.name }}" value="{{ currency.id }}">{{ currency.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<input step="any" required class="form-control" id="amount" value="" autocomplete="off" name="amount" type="number"/>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'close'|_ }}</button>
<button type="submit" class="btn btn-primary">{{ 'set_available_amount'|_ }}</button>
</div>
</form>
</div>
</div>

View File

@ -1,37 +0,0 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span>&times;</span><span class="sr-only">{{ 'close'|_ }}</span>
</button>
<h4 class="modal-title">
{{ trans('firefly.update_budget_amount_range',
{start: start.isoFormat(monthAndDayFormat), end: end.isoFormat(monthAndDayFormat)}) }}
</h4>
</div>
<form style="display: inline;" id="income" action="{{ route('available-budgets.store') }}" method="POST">
<div class="modal-body">
<input type="hidden" name="_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="start" value="{{ start.format('Y-m-d') }}"/>
<input type="hidden" name="end" value="{{ end.format('Y-m-d') }}"/>
<input type="hidden" name="page" value="{{ page }}"/>
<input type="hidden" name="currency_id" value="{{ currency.id }}"/>
<div class="form-group">
<p class="form-control-static">{{ trans('firefly.ab_basic_modal_explain', {currency: currency.name}) }}</p>
</div>
<div class="input-group">
<div class="input-group-addon">{{ currency.symbol|raw }}</div>
<input step="any" required class="form-control" id="amount" value="" autocomplete="off" name="amount" type="number"/>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'close'|_ }}</button>
<button type="submit" class="btn btn-primary">{{ 'set_available_amount'|_ }}</button>
</div>
</form>
</div>
</div>

View File

@ -1,33 +0,0 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span>&times;</span><span class="sr-only">{{ 'close'|_ }}</span>
</button>
<h4 class="modal-title">
{{ trans('firefly.update_budget_amount_range',
{start: availableBudget.start_date.isoFormat(monthAndDayFormat), end: availableBudget.end_date.isoFormat(monthAndDayFormat)}) }}
</h4>
</div>
<form style="display: inline;" id="income" action="{{ route('available-budgets.update', [availableBudget.id, start.format('Y-m-d'), end.format('Y-m-d')]) }}" method="POST">
<div class="modal-body">
<input type="hidden" name="_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="page" value="{{ page }}"/>
<div class="form-group">
<p class="form-control-static">{{ trans('firefly.ab_basic_modal_explain', {currency: availableBudget.transactionCurrency.name}) }}</p>
</div>
<div class="input-group">
<div class="input-group-addon">{{ availableBudget.transactionCurrency.symbol|raw }}</div>
<input step="any" required class="form-control" id="amount" value="{{ availableBudget.amount }}" autocomplete="off" name="amount" type="number"/>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'close'|_ }}</button>
<button type="submit" class="btn btn-primary">{{ 'update_available_amount'|_ }}</button>
</div>
</form>
</div>
</div>

View File

@ -77,7 +77,6 @@
:
<span class="available_amount" data-id="0" data-value="0" data-currency="{{ defaultCurrency.id }}"
data-value="0">{{ formatAmountBySymbol(0, defaultCurrency.symbol, defaultCurrency.decimal_places, true) }}</span>
<a href="#" data-id="0" class="update_ab btn btn-default btn-xs"><span class="fa fa-pencil"></span></a>
</small>
</div>
</div>
@ -90,15 +89,6 @@
</div>
</div>
</div>
{% if enableAddButton %}
<div class="col-lg-12">
<p class="pull-right">
<a href="#" class="btn btn-light btn-xs create_ab_alt">
<span class="fa fa-plus-circle"></span>
{{ 'alt_currency_ab_create'|_ }}</a>
</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
@ -134,10 +124,8 @@
<small class="available_bar"
data-id="{{ budget.id }}">{{ trans('firefly.available_between', {start: budget.start_date.isoFormat(monthAndDayFormat), end: budget.end_date.isoFormat(monthAndDayFormat) }) }}
:
<span class="available_amount" data-id="{{ budget.id }}"
<span class="available_amount" data-id="{{ budget.id }}" data-currency="{{ budget.transaction_currency.id }}"
data-value="{{ budget.amount }}">{{ formatAmountBySymbol(budget.amount, budget.transaction_currency.symbol, budget.transaction_currency.decimal_places, true) }}</span>
<a href="#" data-id="{{ budget.id }}" class="update_ab btn btn-default btn-xs"><span class="fa fa-pencil"></span></a>
<a href="#" data-id="{{ budget.id }}" class="delete_ab btn btn-danger btn-xs"><span class="fa fa-trash"></span></a>
</small>
</div>
</div>
@ -190,15 +178,6 @@
</div>
</div>
{% endfor %}
{% if enableAddButton %}
<div class="col-lg-12">
<p class="pull-right">
<a href="#" class="btn btn-light btn-xs create_ab_alt">
<span class="fa fa-plus-circle"></span>
{{ 'alt_currency_ab_create'|_ }}</a>
</p>
</div>
{% endif %}
</div>
{% endif %}
{% if budgets|length == 0 and inactive.count() == 0 %}
@ -206,7 +185,7 @@
{# make FF ignore demo for now. #}
{% set shownDemo = true %}
{% else %}
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="box">
@ -460,12 +439,6 @@
// index route.
var budgetIndexUrl = "{{ route('budgets.index',['START','END']) }}";
// create available budgets / edit
var createAvailableBudgetUrl = "{{ route('available-budgets.create', [start.format('Y-m-d'), end.format('Y-m-d')]) }}";
var createAltAvailableBudgetUrl = "{{ route('available-budgets.create-alternative', [start.format('Y-m-d'), end.format('Y-m-d')]) }}";
var editAvailableBudgetUrl = "{{ route('available-budgets.edit', ['REPLACEME', start.format('Y-m-d'), end.format('Y-m-d')]) }}";
var deleteABUrl = "{{ route('available-budgets.delete') }}";
// budget limit create form.
var createBudgetLimitUrl = "{{ route('budget-limits.create', ['REPLACEME', start.format('Y-m-d'), end.format('Y-m-d')]) }}";
var storeBudgetLimitUrl = "{{ route('budget-limits.store') }}";

View File

@ -260,29 +260,6 @@ Route::group(
}
);
/**
* Available Budget Controller.
*/
Route::group(
['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers', 'prefix' => 'available-budgets', 'as' => 'available-budgets.'],
static function () {
// create
Route::get('create/{start_date}/{end_date}/{currency?}', ['uses' => 'Budget\AvailableBudgetController@create', 'as' => 'create']);
Route::get(
'create-alternative/{start_date}/{end_date}',
['uses' => 'Budget\AvailableBudgetController@createAlternative', 'as' => 'create-alternative']
);
Route::post('store', ['uses' => 'Budget\AvailableBudgetController@store', 'as' => 'store']);
// edit
Route::get('edit/{availableBudget}/{start_date}/{end_date}', ['uses' => 'Budget\AvailableBudgetController@edit', 'as' => 'edit']);
Route::post('update/{availableBudget}/{start_date}/{end_date}', ['uses' => 'Budget\AvailableBudgetController@update', 'as' => 'update']);
Route::post('delete', ['uses' => 'Budget\AvailableBudgetController@delete', 'as' => 'delete']);
}
);
/**
* Budget Limit Controller.
*/