diff --git a/app/Http/Controllers/Budget/BudgetLimitController.php b/app/Http/Controllers/Budget/BudgetLimitController.php new file mode 100644 index 0000000000..750dd1523c --- /dev/null +++ b/app/Http/Controllers/Budget/BudgetLimitController.php @@ -0,0 +1,124 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Budget; + + +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Repositories\Budget\AvailableBudgetRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; + +/** + * + * Class BudgetLimitController + */ +class BudgetLimitController extends Controller +{ + + /** @var AvailableBudgetRepositoryInterface */ + private $abRepository; + /** @var BudgetLimitRepositoryInterface */ + private $blRepository; + /** @var CurrencyRepositoryInterface */ + private $currencyRepos; + /** @var OperationsRepositoryInterface */ + private $opsRepository; + /** @var BudgetRepositoryInterface The budget repository */ + private $repository; + + /** + * 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-tasks'); + $this->repository = app(BudgetRepositoryInterface::class); + $this->opsRepository = app(OperationsRepositoryInterface::class); + $this->abRepository = app(AvailableBudgetRepositoryInterface::class); + $this->blRepository = app(BudgetLimitRepositoryInterface::class); + $this->currencyRepos = app(CurrencyRepositoryInterface::class); + + return $next($request); + } + ); + } + + /** + * @param Request $request + * @param BudgetLimit $budgetLimit + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function delete(Request $request, BudgetLimit $budgetLimit) + { + $this->blRepository->destroyBudgetLimit($budgetLimit); + session()->flash('success', trans('firefly.deleted_bl')); + + return redirect(route('budgets.index')); + } + + /** + * @param Request $request + * + * @return JsonResponse + */ + public function store(Request $request): JsonResponse + { + $limit = $this->blRepository->store( + [ + 'budget_id' => $request->get('budget_id'), + 'transaction_currency_id' => $request->get('transaction_currency_id'), + 'start_date' => $request->get('start'), + 'end_date' => $request->get('end'), + 'amount' => $request->get('amount'), + ] + ); + + return response()->json($limit->toArray()); + } + + /** + * @param Request $request + * @param BudgetLimit $budgetLimit + * + * @return JsonResponse + */ + public function update(Request $request, BudgetLimit $budgetLimit): JsonResponse + { + $amount = $request->get('amount'); + + return response()->json($this->blRepository->update($budgetLimit, ['amount' => $amount])->toArray()); + + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/Budget/IndexController.php b/app/Http/Controllers/Budget/IndexController.php index c41528a14f..8ce2683702 100644 --- a/app/Http/Controllers/Budget/IndexController.php +++ b/app/Http/Controllers/Budget/IndexController.php @@ -27,6 +27,8 @@ namespace FireflyIII\Http\Controllers\Budget; use Carbon\Carbon; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\AvailableBudget; +use FireflyIII\Models\Budget; +use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Budget\AvailableBudgetRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface; @@ -36,7 +38,7 @@ use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Support\Http\Controllers\DateCalculation; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; use Log; /** @@ -101,17 +103,15 @@ class IndexController extends Controller $range = app('preferences')->get('viewRange', '1M')->data; $start = $start ?? session('start', Carbon::now()->startOfMonth()); $end = $end ?? app('navigation')->endOfPeriod($start, $range); - $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; $defaultCurrency = app('amount')->getDefaultCurrency(); $budgeted = '0'; $spent = '0'; + + // new period stuff: $periodTitle = app('navigation')->periodShow($start, $range); - - // loop of previous periods: - $prevLoop = $this->getPreviousPeriods($start, $range); - $nextLoop = $this->getNextPeriods($start, $range); + $prevLoop = $this->getPreviousPeriods($start, $range); + $nextLoop = $this->getNextPeriods($start, $range); // get all available budgets. $ab = $this->abRepository->get($start, $end); @@ -152,17 +152,46 @@ class IndexController extends Controller // get all budgets, and paginate them into $budgets. $collection = $this->repository->getActiveBudgets(); - $total = $collection->count(); - $budgets = $collection->slice(($page - 1) * $pageSize, $pageSize); + $budgets = []; + + // complement budget with budget limits in range, and expenses in currency X in range. + /** @var Budget $current */ + foreach ($collection as $current) { + $array = $current->toArray(); + $array['spent'] = []; + $array['budgeted'] = []; + $budgetLimits = $this->blRepository->getBudgetLimits($current, $start, $end); + + /** @var BudgetLimit $limit */ + foreach ($budgetLimits as $limit) { + $array['budgeted'][] = [ + 'id' => $limit->id, + 'amount' => $limit->amount, + 'currency_id' => $limit->transactionCurrency->id, + 'currency_symbol' => $limit->transactionCurrency->symbol, + 'currency_name' => $limit->transactionCurrency->name, + 'currency_decimal_places' => $limit->transactionCurrency->decimal_places, + ]; + } + + /** @var TransactionCurrency $currency */ + foreach ($currencies as $currency) { + $spentArr = $this->opsRepository->sumExpenses($start, $end, null, new Collection([$current]), $currency); + if (isset($spentArr[$currency->id]['sum'])) { + $array['spent'][$currency->id]['spent'] = $spentArr[$currency->id]['sum']; + $array['spent'][$currency->id]['currency_id'] = $currency->id; + $array['spent'][$currency->id]['currency_symbol'] = $currency->symbol; + $array['spent'][$currency->id]['currency_decimal_places'] = $currency->decimal_places; + + } + } + $budgets[] = $array; + } // get all inactive budgets, and simply list them: $inactive = $this->repository->getInactiveBudgets(); - // paginate budgets - $paginator = new LengthAwarePaginator($budgets, $total, $pageSize, $page); - $paginator->setPath(route('budgets.index')); - return view( 'budgets.index', compact( 'availableBudgets', @@ -171,11 +200,12 @@ class IndexController extends Controller //'prevText', 'previousLoop', 'nextLoop', 'budgeted', 'spent', 'prevLoop', 'nextLoop', - 'paginator', + 'budgets', + 'currencies', 'enableAddButton', 'periodTitle', 'defaultCurrency', - 'page', 'activeDaysPassed', 'activeDaysLeft', + 'activeDaysPassed', 'activeDaysLeft', 'inactive', 'budgets', 'start', 'end' ) ); diff --git a/app/Models/BudgetLimit.php b/app/Models/BudgetLimit.php index f869bd86d8..02c304911a 100644 --- a/app/Models/BudgetLimit.php +++ b/app/Models/BudgetLimit.php @@ -70,7 +70,7 @@ class BudgetLimit extends Model ]; /** @var array Fields that can be filled */ - protected $fillable = ['budget_id', 'start_date', 'end_date', 'amount']; + protected $fillable = ['budget_id', 'start_date', 'end_date', 'amount','transaction_currency_id']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). diff --git a/app/Models/TransactionCurrency.php b/app/Models/TransactionCurrency.php index a5a2793bda..eb5e50d23d 100644 --- a/app/Models/TransactionCurrency.php +++ b/app/Models/TransactionCurrency.php @@ -40,7 +40,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * @property Carbon $created_at * @property Carbon $updated_at * @property \Illuminate\Support\Carbon|null $deleted_at - * @property string $name * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\BudgetLimit[] $budgetLimits * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournal[] $transactionJournals * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Transaction[] $transactions diff --git a/app/Repositories/Budget/BudgetLimitRepository.php b/app/Repositories/Budget/BudgetLimitRepository.php index 2de5bea6f3..795eb8a688 100644 --- a/app/Repositories/Budget/BudgetLimitRepository.php +++ b/app/Repositories/Budget/BudgetLimitRepository.php @@ -227,14 +227,14 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface $q1->where( static function (Builder $q2) use ($start, $end) { $q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d 00:00:00')); - $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d 00:00:00')); + $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d 23:59:59')); } ) // budget limit start within period ->orWhere( static function (Builder $q3) use ($start, $end) { $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d 00:00:00')); - $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 00:00:00')); + $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 23:59:59')); } ); } @@ -242,7 +242,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface ->orWhere( static function (Builder $q4) use ($start, $end) { // or start is before start AND end is after end. - $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 00:00:00')); + $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 23:59:59')); $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d 00:00:00')); } ); @@ -265,6 +265,17 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface * * @return BudgetLimit */ + public function store(array $data): BudgetLimit + { + return BudgetLimit::create($data); + } + + /** + * @param array $data + * + * @return BudgetLimit + * @deprecated + */ public function storeBudgetLimit(array $data): BudgetLimit { /** @var Budget $budget */ @@ -303,12 +314,27 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface return $limit; } + /** + * @param BudgetLimit $budgetLimit + * @param array $data + * + * @return BudgetLimit + */ + public function update(BudgetLimit $budgetLimit, array $data): BudgetLimit + { + $budgetLimit->amount = $data['amount'] ?? $budgetLimit->amount; + $budgetLimit->save(); + + return $budgetLimit; + } + /** * @param BudgetLimit $budgetLimit * @param array $data * * @return BudgetLimit * @throws Exception + * @deprecated */ public function updateBudgetLimit(BudgetLimit $budgetLimit, array $data): BudgetLimit { diff --git a/app/Repositories/Budget/BudgetLimitRepositoryInterface.php b/app/Repositories/Budget/BudgetLimitRepositoryInterface.php index e92bf4a736..5db47a2865 100644 --- a/app/Repositories/Budget/BudgetLimitRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetLimitRepositoryInterface.php @@ -35,6 +35,19 @@ use Illuminate\Support\Collection; */ interface BudgetLimitRepositoryInterface { + /** + * Tells you which amount has been budgeted (for the given budgets) + * in the selected query. Returns a positive amount as a string. + * + * @param Carbon $start + * @param Carbon $end + * @param TransactionCurrency $currency + * @param Collection|null $budgets + * + * @return string + */ + public function budgeted(Carbon $start, Carbon $end, TransactionCurrency $currency, ?Collection $budgets = null): string; + /** * Destroy a budget limit. * @@ -80,20 +93,15 @@ interface BudgetLimitRepositoryInterface * * @return BudgetLimit */ - public function storeBudgetLimit(array $data): BudgetLimit; + public function store(array $data): BudgetLimit; /** - * Tells you which amount has been budgeted (for the given budgets) - * in the selected query. Returns a positive amount as a string. + * @param array $data * - * @param Carbon $start - * @param Carbon $end - * @param TransactionCurrency $currency - * @param Collection|null $budgets - * - * @return string + * @return BudgetLimit + * @deprecated */ - public function budgeted(Carbon $start, Carbon $end, TransactionCurrency $currency, ?Collection $budgets = null): string; + public function storeBudgetLimit(array $data): BudgetLimit; /** * @param BudgetLimit $budgetLimit @@ -101,6 +109,15 @@ interface BudgetLimitRepositoryInterface * * @return BudgetLimit */ + public function update(BudgetLimit $budgetLimit, array $data): BudgetLimit; + + /** + * @param BudgetLimit $budgetLimit + * @param array $data + * + * @return BudgetLimit + * @deprecated + */ public function updateBudgetLimit(BudgetLimit $budgetLimit, array $data): BudgetLimit; /** diff --git a/public/v1/js/ff/budgets/index.js b/public/v1/js/ff/budgets/index.js index 2e68214085..52b6ca8da5 100644 --- a/public/v1/js/ff/budgets/index.js +++ b/public/v1/js/ff/budgets/index.js @@ -30,10 +30,15 @@ $(function () { On start, fill the "spent"-bar using the content from the page. */ //drawSpentBar(); + drawSpentBars(); //drawBudgetedBar(); - $('.update_ab').on('click', updateAvailableBudget) - $('.create_ab_alt').on('click', createAltAvailableBudget) + drawBudgetedBars(); + + $('.update_ab').on('click', updateAvailableBudget); + $('.create_ab_alt').on('click', createAltAvailableBudget); + + $('.budget_amount').on('change', updateBudgetedAmount); /* When the input changes, update the percentages for the budgeted bar: @@ -78,6 +83,38 @@ $(function () { } }); +function updateBudgetedAmount(e) { + var input = $(e.currentTarget); + var budgetId = parseInt(input.data('id')); + var budgetLimitId = parseInt(input.data('limit')); + var currencyId = parseInt(input.data('currency')); + console.log(budgetLimitId); + if (0 === budgetLimitId) { + $.post(createBudgetLimitUri, { + _token: token, + budget_id: budgetId, + transaction_currency_id: currencyId, + amount: input.val(), + start: periodStart, + end: periodEnd + }).done(function (data) { + + alert('done!'); + }).fail(function () { + alert('I failed :('); + }); + } else { + $.post(updateBudgetLimitUri.replace('REPLACEME', budgetLimitId), { + _token: token, + amount: input.val(), + }).done(function (data) { + alert('done!'); + }).fail(function () { + alert('I failed :('); + }); + } +} + var fixHelper = function (e, tr) { "use strict"; var $originals = tr.children(); @@ -110,13 +147,15 @@ function sortStop(event, ui) { }; $.post('budgets/reorder', arr); } + function createAltAvailableBudget(e) { var button = $(e.currentTarget); - $('#defaultModal').empty().load(createAltAvailableBudgetUri, function () { - $('#defaultModal').modal('show'); - }); + $('#defaultModal').empty().load(createAltAvailableBudgetUri, function () { + $('#defaultModal').modal('show'); + }); return false; } + function updateAvailableBudget(e) { var button = $(e.currentTarget); var abId = parseInt(button.data('id')); @@ -135,6 +174,55 @@ function updateAvailableBudget(e) { } +function drawBudgetedBars() { + "use strict"; + $.each($('.budgeted_bar'), function (i, v) { + var bar = $(v); + var budgeted = parseFloat(bar.data('budgeted')); + var available = parseFloat(bar.data('available')); + console.log('Budgeted bar for bar ' + bar.data('id')); + var budgetedTooMuch = budgeted > available; + var pct; + if (budgetedTooMuch) { + console.log('over budget'); + // budgeted too much. + pct = (available / budgeted) * 100; + bar.find('.progress-bar-warning').css('width', pct + '%'); + bar.find('.progress-bar-danger').css('width', (100 - pct) + '%'); + bar.find('.progress-bar-info').css('width', 0); + } else { + console.log('under budget'); + pct = (budgeted / available) * 100; + bar.find('.progress-bar-warning').css('width', 0); + bar.find('.progress-bar-danger').css('width', 0); + bar.find('.progress-bar-info').css('width', pct + '%'); + } + //$('#budgetedAmount').html(currencySymbol + ' ' + budgeted.toFixed(2)); + }); +} + +function drawSpentBars() { + "use strict"; + $.each($('.spent_bar'), function (i, v) { + var bar = $(v); + var spent = parseFloat(bar.data('spent')) * -1; + var budgeted = parseFloat(bar.data('budgeted')); + var overspent = spent > budgeted; + var pct; + + if (overspent) { + // draw overspent bar + pct = (budgeted / spent) * 100; + bar.find('.progress-bar-warning').css('width', pct + '%'); + bar.find('.progress-bar-danger').css('width', (100 - pct) + '%'); + } else { + // draw normal bar: + pct = (spent / budgeted) * 100; + bar.find('.progress-bar-info').css('width', pct + '%'); + } + }); +} + // // // function drawSpentBar() { diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 54d67146d3..5d827f2d9b 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -688,7 +688,9 @@ return [ 'set_ab' => 'The available budget amount has been set', 'updated_ab' => 'The available budget amount has been updated', 'deleted_ab' => 'The available budget amount has been deleted', + 'deleted_bl' => 'The budgeted amount has been removed', 'alt_currency_ab_create' => 'Set the available budget in another currency', + 'bl_create_btn' => 'Set budget in another currency', 'inactiveBudgets' => 'Inactive budgets', 'without_budget_between' => 'Transactions without a budget between :start and :end', 'delete_budget' => 'Delete budget ":name"', diff --git a/resources/views/v1/budgets/index.twig b/resources/views/v1/budgets/index.twig index e7b4287f6f..eb311ee5e1 100644 --- a/resources/views/v1/budgets/index.twig +++ b/resources/views/v1/budgets/index.twig @@ -145,7 +145,8 @@ {# progresss bar to visualise available vs budgeted. #}
- - {% if budgetInformation[budget.id]['currentLimit'] %} - {{ budget.name }} - {% else %} - {{ budget.name }} + {{ budget.name }} + | +
+ {% if 0==budget.budgeted|length %}
+
+
+
+ {% endif %}
+ {% if budget.budgeted|length > 0 %}
+ {% for budgetLimit in budget.budgeted %}
+ {{ defaultCurrency.symbol }}
+
+
+
+
+
+ {% endfor %}
+ {% endif %}
+ {% if budget.budgeted|length < currencies.count %}
+
+
+ {{ 'bl_create_btn'|_ }}
{% endif %}
{{ budgetLimit.currency_symbol }}
+
+
+
+
+
+ |
- {% if budgetInformation[budget.id]['currentLimit'] %}
- {% set repAmount = budgetInformation[budget.id]['budgeted'] %}
- {% else %}
- {% set repAmount = '0' %}
- {% endif %}
+
-
-
-
{{ defaultCurrency.symbol|raw }}
-
-
- |
-
- - {{ "-1"|formatAmount }} + | + + {% if budget.budgeted|length > 0 %} + {% for budgetLimit in budget.budgeted %} + {% for spentInfo in budget.spent %} + {% if spentInfo.currency_id == budgetLimit.currency_id %} + {{ formatAmountBySymbol(spentInfo.spent + budgetLimit.amount, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }} + {% if spentInfo.spent + budgetLimit.amount > 0 %} + ({{ formatAmountBySymbol((spentInfo.spent + budgetLimit.amount) / activeDaysLeft, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }}) + {% else %} + ({{ formatAmountBySymbol(0, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }}) + {% endif %} + {% endif %} + {% endfor %} + + {% endfor %} + {% endif %} + + {#{{ "-1"|formatAmount }}#} {#{{ (repAmount + budgetInformation[budget.id]['spent'])|formatAmount }}#} - {% if repAmount + budgetInformation[budget.id]['spent'] > 0 %} - ({{ "-1"|formatAmount }}) - {#({{ ((repAmount + budgetInformation[budget.id]['spent']) / activeDaysLeft)|formatAmount }})#} - {% endif %} + {#{% if repAmount + budgetInformation[budget.id]['spent'] > 0 %}#} + {#({{ "-1"|formatAmount }})#} + {#({{ ((repAmount + budgetInformation[budget.id]['spent']) / activeDaysLeft)|formatAmount }})#} + {#{% endif %}#} |