diff --git a/app/Console/Commands/Integrity/CreateGroupMemberships.php b/app/Console/Commands/Integrity/CreateGroupMemberships.php index 0927f1b7cb..2bfb099912 100644 --- a/app/Console/Commands/Integrity/CreateGroupMemberships.php +++ b/app/Console/Commands/Integrity/CreateGroupMemberships.php @@ -30,8 +30,10 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\GroupMembership; use FireflyIII\Models\UserGroup; use FireflyIII\Models\UserRole; +use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Log; /** * Class CreateGroupMemberships @@ -41,8 +43,9 @@ class CreateGroupMemberships extends Command use ShowsFriendlyMessages; public const string CONFIG_NAME = '560_create_group_memberships'; - protected $description = 'Update group memberships'; - protected $signature = 'firefly-iii:create-group-memberships'; + protected $description = 'Update group memberships'; + protected $signature = 'firefly-iii:create-group-memberships'; + /** * Execute the console command. @@ -52,6 +55,7 @@ class CreateGroupMemberships extends Command public function handle(): int { $this->createGroupMemberships(); + $this->setDefaultGroups(); $this->friendlyPositive('Validated group memberships'); return 0; @@ -78,20 +82,19 @@ class CreateGroupMemberships extends Command public static function createGroupMembership(User $user): void { // check if membership exists - $userGroup = UserGroup::where('title', $user->email)->first(); + $userGroup = UserGroup::where('title', $user->email)->first(); if (null === $userGroup) { - $userGroup = UserGroup::create(['title' => $user->email]); + $userGroup = UserGroup::create(['title' => $user->email, 'default_administration' => true]); } - $userRole = UserRole::where('title', UserRoleEnum::OWNER->value)->first(); + $userRole = UserRole::where('title', UserRoleEnum::OWNER->value)->first(); if (null === $userRole) { throw new FireflyException('Firefly III could not find a user role. Please make sure all migrations have run.'); } $membership = GroupMembership::where('user_id', $user->id) - ->where('user_group_id', $userGroup->id) - ->where('user_role_id', $userRole->id)->first() - ; + ->where('user_group_id', $userGroup->id) + ->where('user_role_id', $userRole->id)->first(); if (null === $membership) { GroupMembership::create( [ @@ -106,4 +109,71 @@ class CreateGroupMemberships extends Command $user->save(); } } + + private function setDefaultGroups(): void + { + $users = User::get(); + + /** @var User $user */ + foreach ($users as $user) { + $this->setDefaultGroup($user); + } + } + + /** + * @throws FireflyException + */ + private function setDefaultGroup(User $user): void + { + Log::debug(sprintf('setDefaultGroup() for #%d "%s"', $user->id, $user->email)); + /** @var UserRepositoryInterface $repository */ + $repository = app(UserRepositoryInterface::class); + $groups = $repository->getUserGroups($user); + if(1 === $groups->count()) { + /** @var UserGroup $first */ + $first = $groups->first(); + $first->default_administration = true; + $first->save(); + Log::debug(sprintf('User has only one group (#%d, "%s"), make it the default (owner or not).', $first->id, $first->title)); + return; + } + Log::debug(sprintf('User has %d groups.', $groups->count())); + /* + * Loop all the groups, expect to find at least ONE + * where you're owner, and it has your name. In that case, it's yours. + * Then we can safely return and stop. + */ + + /** @var UserGroup $group */ + foreach($groups as $group) { + $group->default_administration = false; + $group->save(); + if($group->title === $user->email) { + $roles = $repository->getRolesInGroup($user, $group->id); + Log::debug(sprintf('Group #%d ("%s")', $group->id, $group->title), $roles); + $isOwner = false; + foreach($roles as $role) { + if($role === UserRoleEnum::OWNER->value) { + $isOwner = true; + } + } + if(true === $isOwner) { + // make this group the default, set the rest NOT to be the default: + $group->default_administration = true; + $group->save(); + Log::debug(sprintf('Make group #%d ("%s") the default (is owner + name matches).', $group->id, $group->title)); + return; + } + if(false === $isOwner) { + $this->friendlyWarning(sprintf('User "%s" has a group with matching name (#%d), but is not the owner. User will be given the owner role.', $user->email, $group->id)); + self::createGroupMembership($user); + return; + } + } + } + // if there is no group at all, create it. + $this->friendlyWarning(sprintf('User "%s" has no group with matching name. Will be created.', $user->email)); + self::createGroupMembership($user); + } + } diff --git a/composer.lock b/composer.lock index 79ecbe5ab1..e253c18cd1 100644 --- a/composer.lock +++ b/composer.lock @@ -8897,16 +8897,16 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.12.3", + "version": "v3.12.4", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "aac8f08b73af8c5d2ab6595c8823ddb26d1453f1" + "reference": "e7a9a217512d8f1d07448fd241ce2ac0922ddc2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/aac8f08b73af8c5d2ab6595c8823ddb26d1453f1", - "reference": "aac8f08b73af8c5d2ab6595c8823ddb26d1453f1", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/e7a9a217512d8f1d07448fd241ce2ac0922ddc2c", + "reference": "e7a9a217512d8f1d07448fd241ce2ac0922ddc2c", "shasum": "" }, "require": { @@ -8965,7 +8965,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.12.3" + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.12.4" }, "funding": [ { @@ -8977,7 +8977,7 @@ "type": "github" } ], - "time": "2024-03-31T18:35:30+00:00" + "time": "2024-04-01T09:14:15+00:00" }, { "name": "barryvdh/laravel-ide-helper", diff --git a/database/migrations/2024_03_03_174645_add_indices.php b/database/migrations/2024_03_03_174645_add_indices.php index 489bb4300b..2d1ae810f3 100644 --- a/database/migrations/2024_03_03_174645_add_indices.php +++ b/database/migrations/2024_03_03_174645_add_indices.php @@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Schema; return new class () extends Migration { private const string QUERY_ERROR = 'Could not execute query (table "%s", field "%s"): %s'; - private const string EXPL = 'If the index already exists (see error), this is not an problem. Otherwise, please open a GitHub discussion.'; + private const string EXPL = 'If the index already exists (see error), or if MySQL can\'t do it, this is not an problem. Otherwise, please open a GitHub discussion.'; /** * Run the migrations. diff --git a/database/migrations/2024_04_01_131611_update_user_group_properties.php b/database/migrations/2024_04_01_131611_update_user_group_properties.php new file mode 100644 index 0000000000..21cc9c39c2 --- /dev/null +++ b/database/migrations/2024_04_01_131611_update_user_group_properties.php @@ -0,0 +1,37 @@ +boolean('default_administration')->default(false)->after('title'); + } + } + ); + } catch (QueryException $e) { + app('log')->error(sprintf('Could not execute query: %s', $e->getMessage())); + app('log')->error('If the column or index already exists (see error), this is not an problem. Otherwise, please open a GitHub discussion.'); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/resources/assets/v2/api/v2/model/user-group/post.js b/resources/assets/v2/api/v2/model/user-group/post.js new file mode 100644 index 0000000000..9ae5d03986 --- /dev/null +++ b/resources/assets/v2/api/v2/model/user-group/post.js @@ -0,0 +1,28 @@ +/* + * post.js + * Copyright (c) 2024 james@firefly-iii.org. + * + * This file is part of Firefly III (https://github.com/firefly-iii). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import {api} from "../../../../boot/axios"; + +export default class Post { + post(submission) { + let url = './api/v2/user-groups'; + return api.post(url, submission); + } +} diff --git a/resources/assets/v2/pages/administrations/create.js b/resources/assets/v2/pages/administrations/create.js new file mode 100644 index 0000000000..fc9889467f --- /dev/null +++ b/resources/assets/v2/pages/administrations/create.js @@ -0,0 +1,110 @@ +/* + * template.js + * Copyright (c) 2024 james@firefly-iii.org. + * + * This file is part of Firefly III (https://github.com/firefly-iii). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import '../../boot/bootstrap.js'; +import dates from "../shared/dates.js"; +import Post from "../../api/v2/model/user-group/post.js"; +import i18next from "i18next"; + + +let administrations = function () { + return { + title: '', + errors: { + title: [] + }, + + // notifications + notifications: { + error: { + show: false, text: '', url: '', + }, success: { + show: false, text: '', url: '', + }, wait: { + show: false, text: '', + + } + }, + // state of the form is stored in formState: + formStates: { + isSubmitting: false, + returnHereButton: false, + saveAsNewButton: false, // edit form only + resetButton: false, + }, + + // form behaviour + formBehaviour: { + formType: 'create', // or 'update' + }, + changedTitle() { + + }, + + pageProperties: {}, + submitForm() { + (new Post()).post({title: this.title}).then(response => { + if (this.formStates.returnHereButton) { + this.notifications.success.show = true; + this.notifications.success.text = i18next.t('firefly.new_administration_created', {title: response.data.data.attributes.title}); + this.notifications.success.url = './administrations'; + } + if (this.formStates.resetButton) { + this.title = ''; + } + if (!this.formStates.returnHereButton) { + window.location.href = './administrations?user_group_id=' + parseInt(response.data.data.id) + '&message=created'; + } + }).catch(error => { + console.error(error); + }); + + }, + cancelForm() { + window.location.href = './administrations'; + }, + init() { + + } + } +} + + +let comps = {administrations, dates}; + +function loadPage() { + Object.keys(comps).forEach(comp => { + console.log(`Loading page component "${comp}"`); + let data = comps[comp](); + Alpine.data(comp, () => data); + }); + Alpine.start(); +} + +// wait for load until bootstrapped event is received. +document.addEventListener('firefly-iii-bootstrapped', () => { + console.log('Loaded through event listener.'); + loadPage(); +}); +// or is bootstrapped before event is triggered. +if (window.bootstrapped) { + console.log('Loaded through window variable.'); + loadPage(); +} diff --git a/resources/assets/v2/pages/template.js b/resources/assets/v2/pages/template.js index ce0ac609dd..bf573f2739 100644 --- a/resources/assets/v2/pages/template.js +++ b/resources/assets/v2/pages/template.js @@ -24,6 +24,33 @@ import dates from "./shared/dates.js"; let somethings = function() { return { + // notifications + // TODO duplicate code + notifications: { + error: { + show: false, text: '', url: '', + }, success: { + show: false, text: '', url: '', + }, wait: { + show: false, text: '', + + } + }, + // state of the form is stored in formState: + // TODO duplicate code + formStates: { + isSubmitting: false, + returnHereButton: false, + saveAsNewButton: false, // edit form only + resetButton: false, + }, + + // form behaviour + // TODO duplicate code + formBehaviour: { + formType: 'create', // or 'update' + }, + pageProperties: {}, functionName() { diff --git a/resources/assets/v2/pages/transactions/create.js b/resources/assets/v2/pages/transactions/create.js index ed575f10eb..f324555e5f 100644 --- a/resources/assets/v2/pages/transactions/create.js +++ b/resources/assets/v2/pages/transactions/create.js @@ -73,7 +73,8 @@ let transactions = function () { // form behaviour during transaction formBehaviour: { - formType: 'create', foreignCurrencyEnabled: true, + formType: 'create', + foreignCurrencyEnabled: true, }, // form data (except transactions) is stored in formData diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index b49bcbceab..da3ace3fc1 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -1390,6 +1390,9 @@ return [ 'administration_owner' => 'Administration owner: {{email}}', 'administration_you' => 'Your role: {{role}}', 'other_users_in_admin' => 'Other users in this administration', + 'administrations_create_breadcrumb' => 'Create new financial administration', + 'basic_administration_information' => 'Basic administration information', + 'new_administration_created' => 'New financial administration "{{title}}" has been created', // roles 'administration_role_owner' => 'Owner', diff --git a/resources/views/v2/administrations/create.blade.php b/resources/views/v2/administrations/create.blade.php new file mode 100644 index 0000000000..9c2096f067 --- /dev/null +++ b/resources/views/v2/administrations/create.blade.php @@ -0,0 +1,57 @@ +@extends('layout.v2') +@section('scripts') + @vite(['resources/assets/v2/pages/administrations/create.js']) +@endsection +@section('content') +