From 3766128cb8a1795f0419459e2aed6c12f1a1d061 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Jan 2025 11:34:23 +0100 Subject: [PATCH] Add code for administrations. --- app/Api/V1/Controllers/Controller.php | 48 +++++ .../Models/UserGroup/IndexController.php | 71 +++++++ .../Models/UserGroup/ShowController.php | 63 +++++++ .../Models/UserGroup/UpdateController.php | 70 +++++++ app/Api/V1/Requests/Data/DateRequest.php | 6 +- .../Models/UserGroup/UpdateRequest.php | 66 +++++++ app/Http/Middleware/InterestingMessage.php | 50 +++++ .../UserGroup/UserGroupRepository.php | 43 +++-- app/Transformers/UserGroupTransformer.php | 126 +++++++++++++ config/translations.php | 9 + resources/assets/v1/mix-manifest.json | 13 +- .../assets/v1/src/administrations/edit.js | 39 ++++ .../assets/v1/src/administrations/index.js | 39 ++++ .../src/components/administrations/Edit.vue | 173 ++++++++++++++++++ .../src/components/administrations/Index.vue | 127 +++++++++++++ .../src/components/form/UserGroupCurrency.vue | 107 +++++++++++ .../v1/src/components/webhooks/Edit.vue | 2 +- resources/assets/v1/webpack.mix.js | 4 + resources/lang/en_US/firefly.php | 11 +- resources/lang/en_US/form.php | 1 + resources/lang/en_US/list.php | 1 + resources/lang/en_US/validation.php | 1 + resources/views/administrations/edit.twig | 8 + resources/views/administrations/index.twig | 6 +- routes/api.php | 19 ++ routes/breadcrumbs.php | 30 +-- 26 files changed, 1095 insertions(+), 38 deletions(-) create mode 100644 app/Api/V1/Controllers/Models/UserGroup/IndexController.php create mode 100644 app/Api/V1/Controllers/Models/UserGroup/ShowController.php create mode 100644 app/Api/V1/Controllers/Models/UserGroup/UpdateController.php create mode 100644 app/Api/V1/Requests/Models/UserGroup/UpdateRequest.php create mode 100644 app/Transformers/UserGroupTransformer.php create mode 100644 resources/assets/v1/src/administrations/edit.js create mode 100644 resources/assets/v1/src/administrations/index.js create mode 100644 resources/assets/v1/src/components/administrations/Edit.vue create mode 100644 resources/assets/v1/src/components/administrations/Index.vue create mode 100644 resources/assets/v1/src/components/form/UserGroupCurrency.vue create mode 100644 resources/views/administrations/edit.twig diff --git a/app/Api/V1/Controllers/Controller.php b/app/Api/V1/Controllers/Controller.php index 57fa8f5268..7e67d5218c 100644 --- a/app/Api/V1/Controllers/Controller.php +++ b/app/Api/V1/Controllers/Controller.php @@ -30,12 +30,19 @@ use FireflyIII\Models\Preference; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Steam; +use FireflyIII\Transformers\V2\AbstractTransformer; use FireflyIII\User; +use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; +use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Routing\Controller as BaseController; +use Illuminate\Support\Collection; use League\Fractal\Manager; +use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use League\Fractal\Resource\Collection as FractalCollection; +use League\Fractal\Resource\Item; use League\Fractal\Serializer\JsonApiSerializer; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\ParameterBag; @@ -223,4 +230,45 @@ abstract class Controller extends BaseController return $manager; } + + final protected function jsonApiList(string $key, LengthAwarePaginator $paginator, AbstractTransformer $transformer): array + { + $manager = new Manager(); + $baseUrl = sprintf('%s/api/v1/',request()->getSchemeAndHttpHost()); + + // TODO add stuff to path? + + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + + $objects = $paginator->getCollection(); + + // the transformer, at this point, needs to collect information that ALL items in the collection + // require, like meta-data and stuff like that, and save it for later. + $objects = $transformer->collectMetaData($objects); + $paginator->setCollection($objects); + + $resource = new FractalCollection($objects, $transformer, $key); + $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); + + return $manager->createData($resource)->toArray(); + } + + /** + * Returns a JSON API object and returns it. + * + * @param array|Model $object + */ + final protected function jsonApiObject(string $key, array|Model $object, AbstractTransformer $transformer): array + { + // create some objects: + $manager = new Manager(); + $baseUrl = sprintf('%s/api/v1', request()->getSchemeAndHttpHost()); + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + + $transformer->collectMetaData(new Collection([$object])); + + $resource = new Item($object, $transformer, $key); + + return $manager->createData($resource)->toArray(); + } } diff --git a/app/Api/V1/Controllers/Models/UserGroup/IndexController.php b/app/Api/V1/Controllers/Models/UserGroup/IndexController.php new file mode 100644 index 0000000000..e689687467 --- /dev/null +++ b/app/Api/V1/Controllers/Models/UserGroup/IndexController.php @@ -0,0 +1,71 @@ +middleware( + function ($request, $next) { + $this->repository = app(UserGroupRepositoryInterface::class); + + return $next($request); + } + ); + } + + public function index(DateRequest $request): JsonResponse + { + $administrations = $this->repository->get(); + $pageSize = $this->parameters->get('limit'); + $count = $administrations->count(); + $administrations = $administrations->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + $paginator = new LengthAwarePaginator($administrations, $count, $pageSize, $this->parameters->get('page')); + $transformer = new UserGroupTransformer(); + + $transformer->setParameters($this->parameters); // give params to transformer + + return response() + ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } +} diff --git a/app/Api/V1/Controllers/Models/UserGroup/ShowController.php b/app/Api/V1/Controllers/Models/UserGroup/ShowController.php new file mode 100644 index 0000000000..4300144b6c --- /dev/null +++ b/app/Api/V1/Controllers/Models/UserGroup/ShowController.php @@ -0,0 +1,63 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V1\Controllers\Models\UserGroup; + +use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Models\UserGroup; +use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface; +use FireflyIII\Transformers\UserGroupTransformer; +use Illuminate\Http\JsonResponse; + +/** + * Class ShowController + */ +class ShowController extends Controller +{ + public const string RESOURCE_KEY = 'user_groups'; + private WebhookRepositoryInterface $repository; + + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + $this->repository = app(WebhookRepositoryInterface::class); + $this->repository->setUser(auth()->user()); + + return $next($request); + } + ); + } + + public function show(UserGroup $userGroup): JsonResponse + { + $transformer = new UserGroupTransformer(); + $transformer->setParameters($this->parameters); + + return response() + ->api($this->jsonApiObject(self::RESOURCE_KEY, $userGroup, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE); + } +} diff --git a/app/Api/V1/Controllers/Models/UserGroup/UpdateController.php b/app/Api/V1/Controllers/Models/UserGroup/UpdateController.php new file mode 100644 index 0000000000..665bc1390a --- /dev/null +++ b/app/Api/V1/Controllers/Models/UserGroup/UpdateController.php @@ -0,0 +1,70 @@ +middleware( + function ($request, $next) { + $this->repository = app(UserGroupRepositoryInterface::class); + + return $next($request); + } + ); + } + + public function update(UpdateRequest $request, UserGroup $userGroup): JsonResponse { + app('log')->debug(sprintf('Now in %s', __METHOD__)); + $data = $request->getData(); + $userGroup = $this->repository->update($userGroup, $data); + $userGroup->refresh(); + app('preferences')->mark(); + + $transformer = new UserGroupTransformer(); + $transformer->setParameters($this->parameters); + + return response() + ->api($this->jsonApiObject(self::RESOURCE_KEY, $userGroup, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } + +} diff --git a/app/Api/V1/Requests/Data/DateRequest.php b/app/Api/V1/Requests/Data/DateRequest.php index b46a853027..9c43d91149 100644 --- a/app/Api/V1/Requests/Data/DateRequest.php +++ b/app/Api/V1/Requests/Data/DateRequest.php @@ -55,6 +55,7 @@ class DateRequest extends FormRequest return [ 'start' => $start, 'end' => $end, + 'date' => $this->getCarbonDate('date'), ]; } @@ -64,8 +65,9 @@ class DateRequest extends FormRequest public function rules(): array { return [ - 'start' => 'required|date', - 'end' => 'required|date|after:start', + 'date' => 'date|after:1900-01-01|before:2099-12-31', + 'start' => 'date|after:1900-01-01|before:2099-12-31|before:end|required_with:end', + 'end' => 'date|after:1900-01-01|before:2099-12-31|after:start|required_with:start', ]; } } diff --git a/app/Api/V1/Requests/Models/UserGroup/UpdateRequest.php b/app/Api/V1/Requests/Models/UserGroup/UpdateRequest.php new file mode 100644 index 0000000000..9c25feae6d --- /dev/null +++ b/app/Api/V1/Requests/Models/UserGroup/UpdateRequest.php @@ -0,0 +1,66 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V1\Requests\Models\UserGroup; + +use FireflyIII\Models\UserGroup; +use FireflyIII\Rules\UserGroup\UniqueTitle; +use FireflyIII\Support\Request\ChecksLogin; +use FireflyIII\Support\Request\ConvertsDataTypes; +use Illuminate\Foundation\Http\FormRequest; + +/** + * Class UpdateRequest + */ +class UpdateRequest extends FormRequest +{ + use ChecksLogin; + use ConvertsDataTypes; + + public function getData(): array + { + $fields = [ + 'title' => ['title', 'convertString'], + 'native_currency_id' => ['native_currency_id', 'convertInteger'], + 'native_currency_code' => ['native_currency_code', 'convertString'], + ]; + + return $this->getAllData($fields); + } + + /** + * Rules for this request. + */ + public function rules(): array + { + /** @var UserGroup $userGroup */ + $userGroup = $this->route()->parameter('userGroup'); + + return [ + 'title' => ['required','min:1','max:255'], + 'native_currency_id' => 'exists:transaction_currencies,id', + 'native_currency_code' => 'exists:transaction_currencies,code', + ]; + } +} diff --git a/app/Http/Middleware/InterestingMessage.php b/app/Http/Middleware/InterestingMessage.php index d6ca27466c..f082d95761 100644 --- a/app/Http/Middleware/InterestingMessage.php +++ b/app/Http/Middleware/InterestingMessage.php @@ -26,9 +26,11 @@ namespace FireflyIII\Http\Middleware; use FireflyIII\Models\Account; use FireflyIII\Models\Bill; +use FireflyIII\Models\GroupMembership; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\UserGroup; use FireflyIII\Models\Webhook; use FireflyIII\User; use Illuminate\Http\Request; @@ -53,6 +55,10 @@ class InterestingMessage app('preferences')->mark(); $this->handleGroupMessage($request); } + if ($this->userGroupMessage($request)) { + app('preferences')->mark(); + $this->handleUserGroupMessage($request); + } if ($this->accountMessage($request)) { app('preferences')->mark(); $this->handleAccountMessage($request); @@ -87,6 +93,14 @@ class InterestingMessage return null !== $transactionGroupId && null !== $message; } + private function userGroupMessage(Request $request): bool + { + // get parameters from request. + $transactionGroupId = $request->get('user_group_id'); + $message = $request->get('message'); + + return null !== $transactionGroupId && null !== $message; + } private function handleGroupMessage(Request $request): void { @@ -135,6 +149,42 @@ class InterestingMessage return null !== $accountId && null !== $message; } + private function handleUserGroupMessage(Request $request): void + { + // get parameters from request. + $userGroupId = $request->get('user_group_id'); + $message = $request->get('message'); + + /** @var User $user */ + $user = auth()->user(); + + $userGroup = UserGroup::find($userGroupId); + $valid = false; + $memberships = $user->groupMemberships()->get(); + + /** @var GroupMembership $membership */ + foreach($memberships as $membership) { + if($membership->userGroup->id === $userGroup->id) { + $valid = true; + break; + } + } + if(false === $valid) { + return; + } + + + if ('deleted' === $message) { + session()->flash('success', (string) trans('firefly.flash_administration_deleted', ['title' => $userGroup->title])); + } + if ('created' === $message) { + session()->flash('success', (string) trans('firefly.flash_administration_created', ['title' => $userGroup->title])); + } + if ('updated' === $message) { + session()->flash('success', (string) trans('firefly.flash_administration_updated', ['title' => $userGroup->title])); + } + } + private function handleAccountMessage(Request $request): void { // get parameters from request. diff --git a/app/Repositories/UserGroup/UserGroupRepository.php b/app/Repositories/UserGroup/UserGroupRepository.php index 164caff7da..62759038f2 100644 --- a/app/Repositories/UserGroup/UserGroupRepository.php +++ b/app/Repositories/UserGroup/UserGroupRepository.php @@ -30,6 +30,7 @@ use FireflyIII\Factory\UserGroupFactory; use FireflyIII\Models\GroupMembership; use FireflyIII\Models\UserGroup; use FireflyIII\Models\UserRole; +use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Collection; @@ -49,7 +50,7 @@ class UserGroupRepository implements UserGroupRepositoryInterface /** @var GroupMembership $membership */ foreach ($memberships as $membership) { /** @var null|User $user */ - $user = $membership->user()->first(); + $user = $membership->user()->first(); if (null === $user) { continue; } @@ -78,8 +79,8 @@ class UserGroupRepository implements UserGroupRepositoryInterface // all users are now moved away from user group. // time to DESTROY all objects. // we have to do this one by one to trigger the necessary observers :( - $objects = ['availableBudgets', 'bills', 'budgets', 'categories', 'currencyExchangeRates', 'objectGroups', - 'recurrences', 'rules', 'ruleGroups', 'tags', 'transactionGroups', 'transactionJournals', 'piggyBanks', 'accounts', 'webhooks', + $objects = ['availableBudgets', 'bills', 'budgets', 'categories', 'currencyExchangeRates', 'objectGroups', + 'recurrences', 'rules', 'ruleGroups', 'tags', 'transactionGroups', 'transactionJournals', 'piggyBanks', 'accounts', 'webhooks', ]; foreach ($objects as $object) { foreach ($userGroup->{$object}()->get() as $item) { // @phpstan-ignore-line @@ -106,7 +107,7 @@ class UserGroupRepository implements UserGroupRepositoryInterface /** @var null|UserGroup $group */ $group = $membership->userGroup()->first(); if (null !== $group) { - $groupId = $group->id; + $groupId = $group->id; if (in_array($groupId, array_keys($set), true)) { continue; } @@ -131,14 +132,14 @@ class UserGroupRepository implements UserGroupRepositoryInterface while ($exists && $loop < 10) { $existingGroup = $this->findByName($groupName); if (null === $existingGroup) { - $exists = false; + $exists = false; /** @var null|UserGroup $existingGroup */ $existingGroup = $this->store(['user' => $user, 'title' => $groupName]); } if (null !== $existingGroup) { // group already exists - $groupName = sprintf('%s-%s', $user->email, substr(sha1(rand(1000, 9999).microtime()), 0, 4)); + $groupName = sprintf('%s-%s', $user->email, substr(sha1(rand(1000, 9999) . microtime()), 0, 4)); } ++$loop; } @@ -159,7 +160,7 @@ class UserGroupRepository implements UserGroupRepositoryInterface $data['user'] = $this->user; /** @var UserGroupFactory $factory */ - $factory = app(UserGroupFactory::class); + $factory = app(UserGroupFactory::class); return $factory->create($data); } @@ -186,7 +187,7 @@ class UserGroupRepository implements UserGroupRepositoryInterface return $this->user->groupMemberships()->where('user_group_id', $groupId)->get(); } - public function setUser(null|Authenticatable|User $user): void + public function setUser(null | Authenticatable | User $user): void { app('log')->debug(sprintf('Now in %s', __METHOD__)); if ($user instanceof User) { @@ -198,6 +199,23 @@ class UserGroupRepository implements UserGroupRepositoryInterface { $userGroup->title = $data['title']; $userGroup->save(); + $currency = null; + /** @var CurrencyRepositoryInterface $repository */ + $repository = app(CurrencyRepositoryInterface::class); + + if (array_key_exists('native_currency_code', $data)) { + $repository->setUser($this->user); + $currency = $repository->findByCode($data['native_currency_code']); + } + + if (array_key_exists('native_currency_id', $data) && null === $currency) { + $repository->setUser($this->user); + $currency = $repository->find((int) $data['native_currency_id']); + } + if (null !== $currency) { + $repository->makeDefault($currency); + } + return $userGroup; } @@ -209,11 +227,11 @@ class UserGroupRepository implements UserGroupRepositoryInterface */ public function updateMembership(UserGroup $userGroup, array $data): UserGroup { - $owner = UserRole::whereTitle(UserRoleEnum::OWNER)->first(); + $owner = UserRole::whereTitle(UserRoleEnum::OWNER)->first(); app('log')->debug('in update membership'); /** @var null|User $user */ - $user = null; + $user = null; if (array_key_exists('id', $data)) { /** @var null|User $user */ $user = User::find($data['id']); @@ -252,9 +270,8 @@ class UserGroupRepository implements UserGroupRepositoryInterface if ($membershipCount > 1) { // group has multiple members. How many are owner, except the user we're editing now? $ownerCount = $userGroup->groupMemberships() - ->where('user_role_id', $owner->id) - ->where('user_id', '!=', $user->id)->count() - ; + ->where('user_role_id', $owner->id) + ->where('user_id', '!=', $user->id)->count(); // if there are no other owners and the current users does not get or keep the owner role, refuse. if ( 0 === $ownerCount diff --git a/app/Transformers/UserGroupTransformer.php b/app/Transformers/UserGroupTransformer.php new file mode 100644 index 0000000000..4029158f2e --- /dev/null +++ b/app/Transformers/UserGroupTransformer.php @@ -0,0 +1,126 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Transformers; + +use FireflyIII\Enums\UserRoleEnum; +use FireflyIII\Models\GroupMembership; +use FireflyIII\Models\UserGroup; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Transformers\V2\AbstractTransformer; +use FireflyIII\User; +use Illuminate\Support\Collection; + +/** + * Class UserGroupTransformer + */ +class UserGroupTransformer extends AbstractTransformer +{ + private array $inUse; + private array $memberships; + private array $membershipsVisible; + + public function __construct() + { + $this->memberships = []; + $this->membershipsVisible = []; + $this->inUse = []; + } + + public function collectMetaData(Collection $objects): Collection + { + if (auth()->check()) { + // collect memberships so they can be listed in the group. + /** @var User $user */ + $user = auth()->user(); + + /** @var UserGroup $userGroup */ + foreach ($objects as $userGroup) { + $userGroupId = $userGroup->id; + $this->inUse[$userGroupId] = $user->user_group_id === $userGroupId; + $access = $user->hasRoleInGroupOrOwner($userGroup, UserRoleEnum::VIEW_MEMBERSHIPS) || $user->hasRole('owner'); + $this->membershipsVisible[$userGroupId] = $access; + if ($access) { + $groupMemberships = $userGroup->groupMemberships()->get(); + + /** @var GroupMembership $groupMembership */ + foreach ($groupMemberships as $groupMembership) { + $this->memberships[$userGroupId][] = [ + 'user_id' => (string) $groupMembership->user_id, + 'user_email' => $groupMembership->user->email, + 'role' => $groupMembership->userRole->title, + 'you' => $groupMembership->user_id === $user->id, + ]; + } + } + } + $this->mergeMemberships(); + } + + return $objects; + } + + private function mergeMemberships(): void + { + $new = []; + foreach ($this->memberships as $groupId => $members) { + $new[$groupId] ??= []; + + foreach ($members as $member) { + $mail = $member['user_email']; + $new[$groupId][$mail] ??= [ + 'user_id' => $member['user_id'], + 'user_email' => $member['user_email'], + 'you' => $member['you'], + 'roles' => [], + ]; + $new[$groupId][$mail]['roles'][] = $member['role']; + } + } + $this->memberships = $new; + } + + /** + * Transform the user group. + */ + public function transform(UserGroup $userGroup): array + { + $currency = Amount::getDefaultCurrencyByUserGroup($userGroup); + return [ + 'id' => $userGroup->id, + 'created_at' => $userGroup->created_at->toAtomString(), + 'updated_at' => $userGroup->updated_at->toAtomString(), + 'in_use' => $this->inUse[$userGroup->id] ?? false, + 'title' => $userGroup->title, + 'can_see_members' => $this->membershipsVisible[$userGroup->id] ?? false, + 'members' => array_values($this->memberships[$userGroup->id] ?? []), + 'native_currency_id' => (string) $currency->id, + 'native_currency_name' => $currency->name, + 'native_currency_code' => $currency->code, + 'native_currency_symbol' => $currency->symbol, + 'native_currency_decimal_places' => $currency->decimal_places, + ]; + // if the user has a specific role in this group, then collect the memberships. + } +} diff --git a/config/translations.php b/config/translations.php index f273a40c2d..519b9826ba 100644 --- a/config/translations.php +++ b/config/translations.php @@ -137,6 +137,12 @@ return [ ], 'v1' => [ 'firefly' => [ + 'administrations_page_title', + 'administrations_index_menu', + 'temp_administrations_introduction', + 'administration_currency_form_help', + 'administrations_page_edit_sub_title_js', + 'table', 'welcome_back', 'flash_error', 'flash_warning', @@ -285,6 +291,7 @@ return [ 'url', 'active', 'interest_date', + 'administration_currency', 'title', 'date', 'book_date', @@ -302,7 +309,9 @@ return [ 'rate', ], 'list' => [ + 'title', 'active', + 'native_currency', 'trigger', 'response', 'delivery', diff --git a/resources/assets/v1/mix-manifest.json b/resources/assets/v1/mix-manifest.json index 528ebcb2cf..868c15eb5f 100644 --- a/resources/assets/v1/mix-manifest.json +++ b/resources/assets/v1/mix-manifest.json @@ -10,18 +10,25 @@ "/build/webhooks/show.js": "/build/webhooks/show.js", "/build/exchange-rates/index.js": "/build/exchange-rates/index.js", "/build/exchange-rates/rates.js": "/build/exchange-rates/rates.js", + "/build/administrations/index.js": "/build/administrations/index.js", + "/build/administrations/edit.js": "/build/administrations/edit.js", + "/public/v1/js/administrations/edit.js": "/public/v1/js/administrations/edit.js", + "/public/v1/js/administrations/index.js": "/public/v1/js/administrations/index.js", "/public/v1/js/app.js": "/public/v1/js/app.js", "/public/v1/js/app.js.LICENSE.txt": "/public/v1/js/app.js.LICENSE.txt", "/public/v1/js/app_vue.js": "/public/v1/js/app_vue.js", "/public/v1/js/app_vue.js.LICENSE.txt": "/public/v1/js/app_vue.js.LICENSE.txt", + "/public/v1/js/create.js": "/public/v1/js/create.js", + "/public/v1/js/create.js.LICENSE.txt": "/public/v1/js/create.js.LICENSE.txt", "/public/v1/js/create_transaction.js": "/public/v1/js/create_transaction.js", "/public/v1/js/create_transaction.js.LICENSE.txt": "/public/v1/js/create_transaction.js.LICENSE.txt", + "/public/v1/js/edit.js": "/public/v1/js/edit.js", + "/public/v1/js/edit.js.LICENSE.txt": "/public/v1/js/edit.js.LICENSE.txt", "/public/v1/js/edit_transaction.js": "/public/v1/js/edit_transaction.js", "/public/v1/js/edit_transaction.js.LICENSE.txt": "/public/v1/js/edit_transaction.js.LICENSE.txt", "/public/v1/js/exchange-rates/index.js": "/public/v1/js/exchange-rates/index.js", "/public/v1/js/exchange-rates/index.js.LICENSE.txt": "/public/v1/js/exchange-rates/index.js.LICENSE.txt", "/public/v1/js/exchange-rates/rates.js": "/public/v1/js/exchange-rates/rates.js", - "/public/v1/js/exchange-rates/rates.js.LICENSE.txt": "/public/v1/js/exchange-rates/rates.js.LICENSE.txt", "/public/v1/js/ff/accounts/create.js": "/public/v1/js/ff/accounts/create.js", "/public/v1/js/ff/accounts/edit-reconciliation.js": "/public/v1/js/ff/accounts/edit-reconciliation.js", "/public/v1/js/ff/accounts/edit.js": "/public/v1/js/ff/accounts/edit.js", @@ -90,6 +97,8 @@ "/public/v1/js/ff/transactions/mass/edit-bulk.js": "/public/v1/js/ff/transactions/mass/edit-bulk.js", "/public/v1/js/ff/transactions/mass/edit.js": "/public/v1/js/ff/transactions/mass/edit.js", "/public/v1/js/ff/transactions/show.js": "/public/v1/js/ff/transactions/show.js", + "/public/v1/js/index.js": "/public/v1/js/index.js", + "/public/v1/js/index.js.LICENSE.txt": "/public/v1/js/index.js.LICENSE.txt", "/public/v1/js/lib/Chart.bundle.min.js": "/public/v1/js/lib/Chart.bundle.min.js", "/public/v1/js/lib/accounting.min.js": "/public/v1/js/lib/accounting.min.js", "/public/v1/js/lib/bootstrap-multiselect.js": "/public/v1/js/lib/bootstrap-multiselect.js", @@ -148,6 +157,8 @@ "/public/v1/js/lib/vue.js": "/public/v1/js/lib/vue.js", "/public/v1/js/profile.js": "/public/v1/js/profile.js", "/public/v1/js/profile.js.LICENSE.txt": "/public/v1/js/profile.js.LICENSE.txt", + "/public/v1/js/show.js": "/public/v1/js/show.js", + "/public/v1/js/show.js.LICENSE.txt": "/public/v1/js/show.js.LICENSE.txt", "/public/v1/js/webhooks/create.js": "/public/v1/js/webhooks/create.js", "/public/v1/js/webhooks/create.js.LICENSE.txt": "/public/v1/js/webhooks/create.js.LICENSE.txt", "/public/v1/js/webhooks/edit.js": "/public/v1/js/webhooks/edit.js", diff --git a/resources/assets/v1/src/administrations/edit.js b/resources/assets/v1/src/administrations/edit.js new file mode 100644 index 0000000000..c741d654cd --- /dev/null +++ b/resources/assets/v1/src/administrations/edit.js @@ -0,0 +1,39 @@ +/* + * edit_transactions.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 . + */ + +import Edit from "../components/administrations/Edit"; + +/** + * First we will load Axios via bootstrap.js + * jquery and bootstrap-sass preloaded in app.js + * vue, uiv and vuei18n are in app_vue.js + */ + +require('../bootstrap'); +const i18n = require('../i18n'); + +let props = {}; +const app = new Vue({ + i18n, + el: "#administrations_edit", + render: (createElement) => { + return createElement(Edit, {props: props}) + }, +}); diff --git a/resources/assets/v1/src/administrations/index.js b/resources/assets/v1/src/administrations/index.js new file mode 100644 index 0000000000..d1fc2e1f14 --- /dev/null +++ b/resources/assets/v1/src/administrations/index.js @@ -0,0 +1,39 @@ +/* + * edit_transactions.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 . + */ + +import Index from "../components/administrations/Index"; + +/** + * First we will load Axios via bootstrap.js + * jquery and bootstrap-sass preloaded in app.js + * vue, uiv and vuei18n are in app_vue.js + */ + +require('../bootstrap'); +const i18n = require('../i18n'); + +let props = {}; +const app = new Vue({ + i18n, + el: "#administrations_index", + render: (createElement) => { + return createElement(Index, {props: props}) + }, +}); diff --git a/resources/assets/v1/src/components/administrations/Edit.vue b/resources/assets/v1/src/components/administrations/Edit.vue new file mode 100644 index 0000000000..f5d288d6c6 --- /dev/null +++ b/resources/assets/v1/src/components/administrations/Edit.vue @@ -0,0 +1,173 @@ + + + + + + + diff --git a/resources/assets/v1/src/components/administrations/Index.vue b/resources/assets/v1/src/components/administrations/Index.vue new file mode 100644 index 0000000000..789345cfcd --- /dev/null +++ b/resources/assets/v1/src/components/administrations/Index.vue @@ -0,0 +1,127 @@ + + + + + + + diff --git a/resources/assets/v1/src/components/form/UserGroupCurrency.vue b/resources/assets/v1/src/components/form/UserGroupCurrency.vue new file mode 100644 index 0000000000..fbd483b69d --- /dev/null +++ b/resources/assets/v1/src/components/form/UserGroupCurrency.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/resources/assets/v1/src/components/webhooks/Edit.vue b/resources/assets/v1/src/components/webhooks/Edit.vue index 779c74ee73..b4c65d1eb9 100644 --- a/resources/assets/v1/src/components/webhooks/Edit.vue +++ b/resources/assets/v1/src/components/webhooks/Edit.vue @@ -187,7 +187,7 @@ export default { // post! axios.put('./api/v1/webhooks/' + this.id, data).then((response) => { - let webhookId = response.data.data.id; + let webhookId = parseInt(response.data.data.id); window.location.href = window.previousUrl + '?webhook_id=' + webhookId + '&message=updated'; }).catch((error) => { diff --git a/resources/assets/v1/webpack.mix.js b/resources/assets/v1/webpack.mix.js index 04ce6319dc..cf2a6627c5 100644 --- a/resources/assets/v1/webpack.mix.js +++ b/resources/assets/v1/webpack.mix.js @@ -50,3 +50,7 @@ mix.js('src/webhooks/show.js', 'build/webhooks').vue({version: 2}).copy('build', // exchange rates mix.js('src/exchange-rates/index.js', 'build/exchange-rates').vue({version: 2}); mix.js('src/exchange-rates/rates.js', 'build/exchange-rates').vue({version: 2}); + +// administrations +mix.js('src/administrations/index.js', 'build/administrations').vue({version: 2}); +mix.js('src/administrations/edit.js', 'build/administrations').vue({version: 2}); diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 8d723780fc..c8b74e176b 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -1477,9 +1477,9 @@ return [ // Financial administrations 'administration_index' => 'Financial administration', - 'administrations_index_menu' => 'Financial administration(s)', 'administrations_breadcrumb' => 'Financial administrations', - 'administrations_page_title' => 'Financial administrations', + 'administrations_page_title' => 'Financial administrations', + 'administrations_index_menu' => 'Financial administrations', 'administrations_page_sub_title' => 'Overview', 'create_administration' => 'Create new administration', 'administration_owner' => 'Administration owner: {{email}}', @@ -1491,6 +1491,13 @@ return [ 'new_administration_created' => 'New financial administration "{{title}}" has been created', 'edit_administration_breadcrumb' => 'Edit financial administration ":title"', 'administrations_page_edit_sub_title' => 'Edit financial administration ":title"', + 'administrations_page_edit_sub_title_js' => 'Edit financial administration "{title}"', + 'temp_administrations_introduction' => 'Firefly III will soon get the ability to manage multiple financial administrations. Right now, you only have the one. You can set the title of this administration and its native currency. This replaces the previous setting where you would set your "default currency". This setting is now tied to the financial administration and can be different per administration.', + 'temp_administrations_introduction_edit' =>'Currently, you can only set the "native currency" of the default financial administration. This replaces the "default currency" setting. This setting is now tied to the financial administration and can be different per administration.', + 'administration_currency_form_help' => 'It may take a long time for the page to load if you change the native currency because transaction may need to be converted to your (new) native currency.', + 'flash_administration_updated' => 'Administration ":title" has been updated', + 'flash_administration_created' => 'Administration ":title" has been created', + 'flash_administration_deleted' => 'Administration ":title" has been deleted', // roles 'administration_role_owner' => 'Owner', diff --git a/resources/lang/en_US/form.php b/resources/lang/en_US/form.php index 3d848c95a2..11f4f95dac 100644 --- a/resources/lang/en_US/form.php +++ b/resources/lang/en_US/form.php @@ -26,6 +26,7 @@ declare(strict_types=1); return [ // new user: + 'administration_currency' => 'Native currency', 'bank_name' => 'Bank name', 'bank_balance' => 'Balance', 'current_balance' => 'Current balance', diff --git a/resources/lang/en_US/list.php b/resources/lang/en_US/list.php index 7ce0c16e51..e367b63a4d 100644 --- a/resources/lang/en_US/list.php +++ b/resources/lang/en_US/list.php @@ -29,6 +29,7 @@ return [ 'icon' => 'Icon', 'id' => 'ID', 'create_date' => 'Created at', + 'native_currency' => 'Native currency', 'update_date' => 'Updated at', 'updated_at' => 'Updated at', 'balance_before' => 'Balance before', diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index e2f50d0cd9..02f3b6f961 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -49,6 +49,7 @@ return [ 'date_or_time' => 'The value must be a valid date or time value (ISO 8601).', 'source_equals_destination' => 'The source account equals the destination account.', 'unique_account_number_for_user' => 'It looks like this account number is already in use.', + 'unique_user_group_for_user' => 'It looks like this administration title is already in use.', 'unique_iban_for_user' => 'It looks like this IBAN is already in use.', 'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"', 'deleted_user' => 'Due to security constraints, you cannot register using this email address.', diff --git a/resources/views/administrations/edit.twig b/resources/views/administrations/edit.twig new file mode 100644 index 0000000000..06586ce9da --- /dev/null +++ b/resources/views/administrations/edit.twig @@ -0,0 +1,8 @@ +{% set VUE_SCRIPT_NAME = 'administrations/edit' %} +{% extends './layout/default' %} +{% block breadcrumbs %} + {{ Breadcrumbs.render }} +{% endblock %} +{% block content %} +
+{% endblock %} diff --git a/resources/views/administrations/index.twig b/resources/views/administrations/index.twig index f17b48e192..58e9242075 100644 --- a/resources/views/administrations/index.twig +++ b/resources/views/administrations/index.twig @@ -1,10 +1,8 @@ +{% set VUE_SCRIPT_NAME = 'administrations/index' %} {% extends './layout/default' %} - {% block breadcrumbs %} {{ Breadcrumbs.render }} {% endblock %} {% block content %} - - - +
{% endblock %} diff --git a/routes/api.php b/routes/api.php index 8bb1a61bad..344e14b995 100644 --- a/routes/api.php +++ b/routes/api.php @@ -527,6 +527,25 @@ Route::group( } ); +// User group API routes. +Route::group( + [ + 'namespace' => 'FireflyIII\Api\V1\Controllers\Models\UserGroup', + 'prefix' => 'v1/user-groups', + 'as' => 'api.v1.user-groups.', + ], + static function (): void { + Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']); + Route::get('{userGroup}', ['uses' => 'ShowController@show', 'as' => 'show']); + Route::put('{userGroup}', ['uses' => 'UpdateController@update', 'as' => 'update']); + //Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']); + // Route::put('{userGroup}', ['uses' => 'UpdateController@update', 'as' => 'update']); + // Route::post('{userGroup}/use', ['uses' => 'UpdateController@useUserGroup', 'as' => 'use']); + // Route::put('{userGroup}/update-membership', ['uses' => 'UpdateController@updateMembership', 'as' => 'updateMembership']); + // Route::delete('{userGroup}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); + } +); + // Bills API routes: Route::group( [ diff --git a/routes/breadcrumbs.php b/routes/breadcrumbs.php index 7bfcf92f1a..ab62293133 100644 --- a/routes/breadcrumbs.php +++ b/routes/breadcrumbs.php @@ -1350,25 +1350,25 @@ Breadcrumbs::for( } ); -Breadcrumbs::for( - 'administrations.show', - static function (Generator $breadcrumbs, UserGroup $userGroup): void { - $breadcrumbs->parent('administrations.index'); - $breadcrumbs->push(limitStringLength($userGroup->title), route('administrations.show', [$userGroup->id])); - } -); +//Breadcrumbs::for( +// 'administrations.show', +// static function (Generator $breadcrumbs, UserGroup $userGroup): void { +// $breadcrumbs->parent('administrations.index'); +// $breadcrumbs->push(limitStringLength($userGroup->title), route('administrations.show', [$userGroup->id])); +// } +//); -Breadcrumbs::for( - 'administrations.create', - static function (Generator $breadcrumbs): void { - $breadcrumbs->parent('administrations.index'); - $breadcrumbs->push(trans('firefly.administrations_create_breadcrumb'), route('administrations.create')); - } -); +//Breadcrumbs::for( +// 'administrations.create', +// static function (Generator $breadcrumbs): void { +// $breadcrumbs->parent('administrations.index'); +// $breadcrumbs->push(trans('firefly.administrations_create_breadcrumb'), route('administrations.create')); +// } +//); Breadcrumbs::for( 'administrations.edit', static function (Generator $breadcrumbs, UserGroup $userGroup): void { - $breadcrumbs->parent('administrations.show', $userGroup); + $breadcrumbs->parent('administrations.index'); $breadcrumbs->push(trans('firefly.edit_administration_breadcrumb', ['title' => limitStringLength($userGroup->title)]), route('administrations.edit', [$userGroup->id])); } );