Add code for administrations.

This commit is contained in:
James Cole
2025-01-19 11:34:23 +01:00
parent 950c60d55c
commit 3766128cb8
26 changed files with 1095 additions and 38 deletions

View File

@@ -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<int, mixed>|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();
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* IndexController.php
* 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/.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\UserGroup;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Data\DateRequest;
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
use FireflyIII\Transformers\UserGroupTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
class IndexController extends Controller
{
public const string RESOURCE_KEY = 'user_groups';
private UserGroupRepositoryInterface $repository;
/**
* AccountController constructor.
*/
public function __construct()
{
parent::__construct();
$this->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)
;
}
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* ShowController.php
* Copyright (c) 2021 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\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);
}
}

View File

@@ -0,0 +1,70 @@
<?php
/*
* UpdateController.php
* Copyright (c) 2025 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\Api\V1\Controllers\Models\UserGroup;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\UserGroup\UpdateRequest;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
use FireflyIII\Transformers\UserGroupTransformer;
use Illuminate\Http\JsonResponse;
class UpdateController extends Controller
{
public const string RESOURCE_KEY = 'user_groups';
private UserGroupRepositoryInterface $repository;
/**
* AccountController constructor.
*/
public function __construct()
{
parent::__construct();
$this->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)
;
}
}

View File

@@ -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',
];
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* UpdateRequest.php
* Copyright (c) 2021 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\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',
];
}
}

View File

@@ -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.

View File

@@ -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

View File

@@ -0,0 +1,126 @@
<?php
/*
* UserGroupTransformer.php
* Copyright (c) 2023 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\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.
}
}

View File

@@ -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',

View File

@@ -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",

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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})
},
});

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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})
},
});

View File

@@ -0,0 +1,173 @@
<!--
- Index.vue
- Copyright (c) 2022 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/>.
-->
<template>
<div>
<form accept-charset="UTF-8" class="form-horizontal" enctype="multipart/form-data">
<input name="_token" type="hidden" value="xxx">
<div v-if="error_message !== ''" class="row">
<div class="col-lg-12">
<div class="alert alert-danger alert-dismissible" role="alert">
<button class="close" data-dismiss="alert" type="button" v-bind:aria-label="$t('firefly.close')"><span
aria-hidden="true">&times;</span></button>
<strong>{{ $t("firefly.flash_error") }}</strong> {{ error_message }}
</div>
</div>
</div>
<div v-if="success_message !== ''" class="row">
<div class="col-lg-12">
<div class="alert alert-success alert-dismissible" role="alert">
<button class="close" data-dismiss="alert" type="button" v-bind:aria-label="$t('firefly.close')"><span
aria-hidden="true">&times;</span></button>
<strong>{{ $t("firefly.flash_success") }}</strong> <span v-html="success_message"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">
{{ $t('firefly.administrations_page_edit_sub_title_js', {title: this.pageTitle}) }}
</h3>
</div>
<div class="box-body">
{{ $t('firefly.temp_administrations_introduction') }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">
{{ $t('firefly.administrations_page_edit_sub_title_js', {title: this.pageTitle}) }}
</h3>
</div>
<div class="box-body">
<Title :value=administration.title :error="errors.title" v-on:input="administration.title = $event"></Title>
<UserGroupCurrency :value=administration.currency_id :error="errors.currency_id"
v-on:input="administration.currency_id = $event"></UserGroupCurrency>
</div>
<div class="box-footer">
<div class="btn-group">
<button id="submitButton" ref="submitButton" class="btn btn-success" @click="submit">
{{ $t('firefly.submit') }}
</button>
</div>
<p class="text-success" v-html="success_message"></p>
<p class="text-danger" v-html="error_message"></p>
</div>
</div>
</div>
</div>
</form>
</div>
</template>
<script>
import Title from "../form/Title.vue";
import WebhookTrigger from "../form/WebhookTrigger.vue";
import UserGroupCurrency from "../form/UserGroupCurrency.vue";
export default {
name: "Edit",
components: {UserGroupCurrency, WebhookTrigger, Title},
data() {
return {
pageTitle: '',
administration: {
title: '',
currency_id: 0,
},
errors: {
title: [],
currency_id: [],
},
error_message: '',
success_message: '',
};
},
mounted() {
const page = window.location.href.split('/');
const administrationId = parseInt(page[page.length - 1]);
this.downloadAdministration(administrationId);
},
methods: {
downloadAdministration: function (id) {
axios.get("./api/v1/user-groups/" + id).then((response) => {
let current = response.data.data;
this.administration = {
id: current.id,
title: current.attributes.title,
currency_id: parseInt(current.attributes.native_currency_id),
currency_code: current.attributes.native_currency_code,
currency_name: current.attributes.native_currency_name,
};
this.pageTitle = this.administration.title;
});
},
submit: function (e) {
// reset messages
this.error_message = '';
this.success_message = '';
this.errors = {
title: [],
currency_id: [],
};
// disable button
$('#submitButton').prop("disabled", true);
// collect data
let data = {
title: this.administration.title,
native_currency_id: parseInt(this.administration.currency_id),
};
// post!
axios.put('./api/v1/user-groups/' + this.administration.id, data).then((response) => {
let administrationId = parseInt(response.data.data.id);
window.location.href = './administrations?user_group_id=' + administrationId + '&message=updated';
}).catch((error) => {
this.error_message = error.response.data.message;
this.errors.title = error.response.data.errors.title;
this.errors.native_currency_id = error.response.data.errors.native_currency_id;
// enable button again
$('#submitButton').prop("disabled", false);
});
if (e) {
e.preventDefault();
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,127 @@
<!--
- Index.vue
- Copyright (c) 2022 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/>.
-->
<template>
<div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">
{{ $t('firefly.administrations_index_menu') }}
</h3>
</div>
<div class="box-body">
{{ $t('firefly.temp_administrations_introduction') }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">
{{ $t('firefly.table') }}
</h3>
</div>
<div class="box-body no-padding">
<table class="table table-responsive table-hover" v-if="administrations.length > 0"
aria-label="A table.">
<thead>
<tr>
<th>{{ $t('list.title') }}</th>
<th>{{ $t('list.native_currency') }}</th>
<th class="hidden-sm hidden-xs">&nbsp;</th>
</tr>
</thead>
<tbody>
<tr v-for="administration in administrations" :key="administration.id">
<td>
<span v-text="administration.title"></span>
</td>
<td>
<span v-text="administration.currency_name"></span> (<span v-text="administration.currency_code"></span>)
</td>
<td class="hidden-sm hidden-xs">
<div class="btn-group btn-group-xs pull-right">
<button type="button" class="btn btn-default dropdown-toggle"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ $t('firefly.actions') }} <span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li><a :href="'./administrations/edit/' + administration.id"><span class="fa fa-fw fa-pencil"></span>
{{ $t('firefly.edit') }}</a></li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Index",
data() {
return {
administrations: [],
};
},
mounted() {
this.getAdministrations();
},
methods: {
getAdministrations: function () {
this.administrations = [];
this.downloadAdministrations(1);
},
downloadAdministrations: function (page) {
axios.get("./api/v1/user-groups?page=" + page).then((response) => {
for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) {
let current = response.data.data[i];
let administration = {
id: current.id,
title: current.attributes.title,
currency_code: current.attributes.native_currency_code,
currency_name: current.attributes.native_currency_name,
};
this.administrations.push(administration);
}
}
if (response.data.meta.pagination.current_page < response.data.meta.pagination.total_pages) {
this.downloadAdministrations(response.data.meta.pagination.current_page + 1);
}
});
},
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,107 @@
<!--
- WebhookDelivery.vue
- Copyright (c) 2022 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/>.
-->
<template>
<div class="form-group" v-bind:class="{ 'has-error': hasError()}">
<label class="col-sm-4 control-label">
{{ $t('form.administration_currency') }}
</label>
<div class="col-sm-8">
<select
v-model="currency"
:title="$t('form.administration_currency')"
class="form-control"
name="user_group_currency"
>
<option v-for="currency in this.currencies"
:label="currency.name"
:value="currency.id">{{ currency.name }}
</option>
</select>
<p class="help-block" v-text="$t('firefly.administration_currency_form_help')"></p>
<ul v-for="error in this.error" class="list-unstyled">
<li class="text-danger">{{ error }}</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: "UserGroupCurrency",
data() {
return {
currency : 0,
currencies: [
],
};
},
props: {
error: {
type: Array,
required: true,
default() {
return []
}
},
value: {
type: Number,
required: true,
}
},
mounted() {
this.currency = this.value;
this.downloadCurrencies(1);
},
watch: {
value() {
this.currency = this.value;
},
currency(newValue) {
this.$emit('input', newValue);
}
},
methods: {
downloadCurrencies: function (page) {
axios.get("./api/v1/currencies?enabled=1&page=" + page).then((response) => {
for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) {
let current = response.data.data[i];
let currency = {
id: current.id,
name: current.attributes.name,
code: current.attributes.code,
};
this.currencies.push(currency);
}
}
if (response.data.meta.pagination.current_page < response.data.meta.pagination.total_pages) {
this.downloadCurrencies(parseInt(response.data.meta.pagination.current_page) + 1);
}
});
},
hasError() {
return this.error?.length > 0;
}
},
}
</script>

View File

@@ -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) => {

View File

@@ -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});

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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.',

View File

@@ -0,0 +1,8 @@
{% set VUE_SCRIPT_NAME = 'administrations/edit' %}
{% extends './layout/default' %}
{% block breadcrumbs %}
{{ Breadcrumbs.render }}
{% endblock %}
{% block content %}
<div id="administrations_edit"></div>
{% endblock %}

View File

@@ -1,10 +1,8 @@
{% set VUE_SCRIPT_NAME = 'administrations/index' %}
{% extends './layout/default' %}
{% block breadcrumbs %}
{{ Breadcrumbs.render }}
{% endblock %}
{% block content %}
<div id="administrations_index"></div>
{% endblock %}

View File

@@ -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(
[

View File

@@ -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]));
}
);