diff --git a/app/Http/Controllers/Account/ReconcileController.php b/app/Http/Controllers/Account/ReconcileController.php
index c16ce9facd..85e47b0be9 100644
--- a/app/Http/Controllers/Account/ReconcileController.php
+++ b/app/Http/Controllers/Account/ReconcileController.php
@@ -130,7 +130,6 @@ class ReconcileController extends Controller
$startDate = clone $start;
$startDate->subDay();
$startBalance = round(app('steam')->balance($account, $startDate), $currency->decimal_places);
-
$endBalance = round(app('steam')->balance($account, $end), $currency->decimal_places);
$subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $account->accountType->type));
$subTitle = (string) trans('firefly.reconcile_account', ['account' => $account->name]);
diff --git a/app/Http/Controllers/ObjectGroup/DeleteController.php b/app/Http/Controllers/ObjectGroup/DeleteController.php
new file mode 100644
index 0000000000..293d953ba6
--- /dev/null
+++ b/app/Http/Controllers/ObjectGroup/DeleteController.php
@@ -0,0 +1,70 @@
+middleware(
+ function ($request, $next) {
+ app('view')->share('mainTitleIcon', 'fa-envelope-o');
+ app('view')->share('title', (string) trans('firefly.object_groups_page_title'));
+
+ $this->repository = app(ObjectGroupRepositoryInterface::class);
+
+ return $next($request);
+ }
+ );
+ }
+
+ /**
+ * Delete a piggy bank.
+ *
+ * @param ObjectGroup $objectGroup
+ */
+ public function delete(ObjectGroup $objectGroup)
+ {
+ $subTitle = (string) trans('firefly.delete_object_group', ['title' => $objectGroup->title]);
+ $piggyBanks = $objectGroup->piggyBanks()->count();
+
+ // put previous url in session
+ $this->rememberPreviousUri('object-groups.delete.uri');
+
+ return view('object-groups.delete', compact('objectGroup', 'subTitle', 'piggyBanks'));
+ }
+
+ /**
+ * Destroy the piggy bank.
+ *
+ * @param ObjectGroup $objectGroup
+ */
+ public function destroy(ObjectGroup $objectGroup): RedirectResponse
+ {
+ session()->flash('success', (string) trans('firefly.deleted_object_group', ['title' => $objectGroup->title]));
+ app('preferences')->mark();
+ $this->repository->destroy($objectGroup);
+
+ return redirect($this->getPreviousUri('object-groups.delete.uri'));
+ }
+
+}
diff --git a/app/Http/Controllers/ObjectGroup/EditController.php b/app/Http/Controllers/ObjectGroup/EditController.php
new file mode 100644
index 0000000000..d760007802
--- /dev/null
+++ b/app/Http/Controllers/ObjectGroup/EditController.php
@@ -0,0 +1,87 @@
+middleware(
+ function ($request, $next) {
+ app('view')->share('mainTitleIcon', 'fa-envelope-o');
+ app('view')->share('title', (string) trans('firefly.object_groups_page_title'));
+
+ $this->repository = app(ObjectGroupRepositoryInterface::class);
+
+ return $next($request);
+ }
+ );
+ }
+
+ /**
+ * Edit an object group.
+ *
+ * @param ObjectGroup $objectGroup
+ */
+ public function edit(ObjectGroup $objectGroup)
+ {
+ $subTitle = (string) trans('firefly.edit_object_group', ['title' => $objectGroup->title]);
+ $subTitleIcon = 'fa-pencil';
+ $targetDate = null;
+ $startDate = null;
+
+ if (true !== session('object-groups.edit.fromUpdate')) {
+ $this->rememberPreviousUri('object-groups.edit.uri');
+ }
+ session()->forget('object-groups.edit.fromUpdate');
+
+ return view('object-groups.edit', compact('subTitle', 'subTitleIcon', 'objectGroup'));
+ }
+
+
+ /**
+ * Update a piggy bank.
+ *
+ * @param ObjectGroupFormRequest $request
+ * @param ObjectGroup $objectGroup
+ */
+ public function update(ObjectGroupFormRequest $request, ObjectGroup $objectGroup)
+ {
+ $data = $request->getObjectGroupData();
+ $piggyBank = $this->repository->update($objectGroup, $data);
+
+ session()->flash('success', (string) trans('firefly.updated_object_group', ['title' => $objectGroup->title]));
+ app('preferences')->mark();
+
+ $redirect = redirect($this->getPreviousUri('object-groups.edit.uri'));
+
+ if (1 === (int) $request->get('return_to_edit')) {
+ // @codeCoverageIgnoreStart
+ session()->put('object-groups.edit.fromUpdate', true);
+
+ $redirect = redirect(route('object-groups.edit', [$piggyBank->id]));
+ // @codeCoverageIgnoreEnd
+ }
+
+ return $redirect;
+ }
+}
diff --git a/app/Http/Controllers/ObjectGroup/IndexController.php b/app/Http/Controllers/ObjectGroup/IndexController.php
new file mode 100644
index 0000000000..6185dade42
--- /dev/null
+++ b/app/Http/Controllers/ObjectGroup/IndexController.php
@@ -0,0 +1,65 @@
+middleware(
+ function ($request, $next) {
+ app('view')->share('mainTitleIcon', 'fa-envelope-o');
+ app('view')->share('title', (string) trans('firefly.object_groups_page_title'));
+ $this->repository = app(ObjectGroupRepositoryInterface::class);
+
+ return $next($request);
+ }
+ );
+ }
+
+ /**
+ * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function index()
+ {
+ $this->repository->sort();
+ $subTitle = (string) trans('firefly.object_groups_index');
+ $objectGroups = $this->repository->get();
+
+ return view('object-groups.index', compact('subTitle', 'objectGroups'));
+ }
+
+ /**
+ * @param ObjectGroup $objectGroup
+ */
+ public function setOrder(Request $request, ObjectGroup $objectGroup)
+ {
+ Log::debug(sprintf('Found object group #%d "%s"', $objectGroup->id, $objectGroup->title));
+ $newOrder = (int) $request->get('order');
+ $this->repository->setOrder($objectGroup, $newOrder);
+
+ return response()->json([]);
+ }
+
+}
diff --git a/app/Http/Requests/ObjectGroupFormRequest.php b/app/Http/Requests/ObjectGroupFormRequest.php
new file mode 100644
index 0000000000..959bc1afaf
--- /dev/null
+++ b/app/Http/Requests/ObjectGroupFormRequest.php
@@ -0,0 +1,75 @@
+.
+ */
+declare(strict_types=1);
+
+namespace FireflyIII\Http\Requests;
+
+use FireflyIII\Models\ObjectGroup;
+
+/**
+ * Class ObjectGroupFormRequest.
+ */
+class ObjectGroupFormRequest extends Request
+{
+ /**
+ * Verify the request.
+ *
+ * @return bool
+ */
+ public function authorize(): bool
+ {
+ // Only allow logged in users
+ return auth()->check();
+ }
+
+ /**
+ * Returns the data required by the controller.
+ *
+ * @return array
+ */
+ public function getObjectGroupData(): array
+ {
+ return [
+ 'title' => $this->string('title'),
+ ];
+ }
+
+ /**
+ * Rules for this request.
+ *
+ * @return array
+ */
+ public function rules(): array
+ {
+ /** @var ObjectGroup $piggy */
+ $objectGroup = $this->route()->parameter('objectGroup');
+
+ $titleRule = 'required|between:1,255|uniqueObjectGroup';
+
+ if (null !== $objectGroup) {
+ $titleRule = sprintf('required|between:1,255|uniqueObjectGroup:%d', $objectGroup->id);
+ }
+
+ return [
+ 'title' => $titleRule,
+ ];
+ }
+}
diff --git a/app/Models/ObjectGroup.php b/app/Models/ObjectGroup.php
index 7e6e03cb26..55e80d82d4 100644
--- a/app/Models/ObjectGroup.php
+++ b/app/Models/ObjectGroup.php
@@ -5,6 +5,7 @@ namespace FireflyIII\Models;
use Illuminate\Database\Eloquent\Model;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class ObjectGroup
@@ -31,6 +32,7 @@ use Illuminate\Database\Eloquent\Model;
class ObjectGroup extends Model
{
protected $fillable = ['title', 'order'];
+
/**
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
@@ -38,4 +40,25 @@ class ObjectGroup extends Model
{
return $this->morphedByMany(PiggyBank::class, 'object_groupable');
}
+
+ /**
+ * Route binder. Converts the key in the URL to the specified object (or throw 404).
+ *
+ * @param string $value
+ *
+ * @throws NotFoundHttpException
+ * @return ObjectGroup
+ */
+ public static function routeBinder(string $value): ObjectGroup
+ {
+ if (auth()->check()) {
+ $objectGroupId = (int) $value;
+ $objectGroup = self::where('object_groups.id', $objectGroupId)
+ ->where('object_groups.user_id', auth()->user()->id)->first();
+ if (null !== $objectGroup) {
+ return $objectGroup;
+ }
+ }
+ throw new NotFoundHttpException;
+ }
}
diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php
index 8417638efc..64770b88ac 100644
--- a/app/Models/PiggyBank.php
+++ b/app/Models/PiggyBank.php
@@ -80,7 +80,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @property-read int|null $piggy_bank_repetitions_count
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\ObjectGroup[] $objectGroups
* @property-read int|null $object_groups_count
- * @property bool $encrypted
*/
class PiggyBank extends Model
{
diff --git a/app/Repositories/ObjectGroup/ObjectGroupRepository.php b/app/Repositories/ObjectGroup/ObjectGroupRepository.php
index afef824a69..6262a038a3 100644
--- a/app/Repositories/ObjectGroup/ObjectGroupRepository.php
+++ b/app/Repositories/ObjectGroup/ObjectGroupRepository.php
@@ -6,14 +6,13 @@ namespace FireflyIII\Repositories\ObjectGroup;
use DB;
use FireflyIII\Models\ObjectGroup;
use Illuminate\Support\Collection;
+use Log;
/**
* Class ObjectGroupRepository
*/
class ObjectGroupRepository implements ObjectGroupRepositoryInterface
{
-
-
/**
* @inheritDoc
*/
@@ -73,4 +72,37 @@ class ObjectGroupRepository implements ObjectGroupRepositoryInterface
$group->save();
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function setOrder(ObjectGroup $objectGroup, int $order): ObjectGroup
+ {
+ $order = 0 === $order ? 1 : $order;
+ $objectGroup->order = $order;
+ $objectGroup->save();
+
+ Log::debug(sprintf('Objectgroup #%d order is now %d', $objectGroup->id, $order));
+
+ return $objectGroup;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function update(ObjectGroup $objectGroup, array $data): ObjectGroup
+ {
+ $objectGroup->title = $data['title'];
+ $objectGroup->save();
+
+ return $objectGroup;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function destroy(ObjectGroup $objectGroup): void
+ {
+ $objectGroup->delete();
+ }
}
diff --git a/app/Repositories/ObjectGroup/ObjectGroupRepositoryInterface.php b/app/Repositories/ObjectGroup/ObjectGroupRepositoryInterface.php
index 6b87378236..b8f42b842b 100644
--- a/app/Repositories/ObjectGroup/ObjectGroupRepositoryInterface.php
+++ b/app/Repositories/ObjectGroup/ObjectGroupRepositoryInterface.php
@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\ObjectGroup;
+use FireflyIII\Models\ObjectGroup;
use Illuminate\Support\Collection;
/**
@@ -32,4 +33,25 @@ interface ObjectGroupRepositoryInterface
*/
public function sort(): void;
+ /**
+ * @param ObjectGroup $objectGroup
+ * @param int $index
+ *
+ * @return ObjectGroup
+ */
+ public function setOrder(ObjectGroup $objectGroup, int $index): ObjectGroup;
+
+ /**
+ * @param ObjectGroup $objectGroup
+ * @param array $data
+ *
+ * @return ObjectGroup
+ */
+ public function update(ObjectGroup $objectGroup, array $data): ObjectGroup;
+
+ /**
+ * @param ObjectGroup $objectGroup
+ */
+ public function destroy(ObjectGroup $objectGroup): void;
+
}
diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php
index 208a6c87f8..8c10713282 100644
--- a/app/Validation/FireflyValidator.php
+++ b/app/Validation/FireflyValidator.php
@@ -505,6 +505,30 @@ class FireflyValidator extends Validator
*
* @return bool
*/
+ public function validateUniqueObjectGroup($attribute, $value, $parameters): bool
+ {
+ $exclude = $parameters[0] ?? null;
+ $query = DB::table('object_groups')
+ ->whereNull('object_groups.deleted_at')
+ ->where('object_groups.user_id', auth()->user()->id)
+ ->where('object_groups.title', $value);
+ if (null !== $exclude) {
+ $query->where('object_groups.id', '!=', (int) $exclude);
+ }
+
+ return 0 === $query->count();
+ }
+
+
+ /**
+ * @param $attribute
+ * @param $value
+ * @param $parameters
+ *
+ * TODO this method does not need a for loop
+ *
+ * @return bool
+ */
public function validateUniquePiggyBankForUser($attribute, $value, $parameters): bool
{
$exclude = $parameters[0] ?? null;
diff --git a/config/firefly.php b/config/firefly.php
index e8e7c950ac..70bd0d3dea 100644
--- a/config/firefly.php
+++ b/config/firefly.php
@@ -31,6 +31,7 @@ use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\Category;
use FireflyIII\Models\LinkType;
+use FireflyIII\Models\ObjectGroup;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\Preference;
use FireflyIII\Models\Recurrence;
@@ -409,6 +410,7 @@ return [
'transactionType' => TransactionTypeModel::class,
'journalLink' => TransactionJournalLink::class,
'currency' => TransactionCurrency::class,
+ 'objectGroup' => ObjectGroup::class,
'piggyBank' => PiggyBank::class,
'preference' => Preference::class,
'tj' => TransactionJournal::class,
diff --git a/database/migrations/2020_06_07_063612_changes_for_v530.php b/database/migrations/2020_06_07_063612_changes_for_v530.php
index fc7d256109..13de0ff70b 100644
--- a/database/migrations/2020_06_07_063612_changes_for_v530.php
+++ b/database/migrations/2020_06_07_063612_changes_for_v530.php
@@ -14,7 +14,7 @@ class ChangesForV530 extends Migration
*
* @return void
*/
- public function down()
+ public function down(): void
{
Schema::dropIfExists('object_groupables');
Schema::dropIfExists('object_groups');
@@ -25,16 +25,18 @@ class ChangesForV530 extends Migration
*
* @return void
*/
- public function up()
+ public function up(): void
{
if (!Schema::hasTable('object_groups')) {
Schema::create(
'object_groups', static function (Blueprint $table) {
$table->increments('id');
+ $table->integer('user_id', false, true);
$table->timestamps();
$table->softDeletes();
$table->string('title', 255);
$table->mediumInteger('order', false, true)->default(0);
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
}
);
}
diff --git a/public/v1/js/ff/object-groups/index.js b/public/v1/js/ff/object-groups/index.js
new file mode 100644
index 0000000000..0529d1404e
--- /dev/null
+++ b/public/v1/js/ff/object-groups/index.js
@@ -0,0 +1,83 @@
+/*
+ * index.js
+ * 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
+ {{ 'object_groups_empty_explain'|_ }} +
++ | + {{ 'object_group_title'|_ }} + | ++ + | +
---|---|---|
+ |
+ {{ objectGroup.title }} + + {% for piggyBank in objectGroup.piggyBanks %} + - {{ 'piggy_bank'|_ }}: {{ piggyBank.name }} + {% endfor %} + |
+ + + | +