From f0fa21deadf11920c775c1609906060c0b2385d0 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 16 Mar 2024 22:00:25 +0100 Subject: [PATCH] First version of line edit. --- .../Model/Account/UpdateController.php | 81 ++++++++++++ .../Request/Model/Account/UpdateRequest.php | 119 ++++++++++++++++++ .../UserGroups/Account/AccountRepository.php | 9 ++ .../Account/AccountRepositoryInterface.php | 2 + .../assets/v2/api/v2/model/account/put.js | 36 ++++++ resources/assets/v2/pages/accounts/index.js | 38 +++++- resources/assets/v2/sass/app.scss | 10 +- .../v2/support/editable/GenericEditor.js | 107 ++++++++++++++++ resources/views/v2/accounts/index.blade.php | 3 + routes/api.php | 1 + 10 files changed, 401 insertions(+), 5 deletions(-) create mode 100644 app/Api/V2/Controllers/Model/Account/UpdateController.php create mode 100644 app/Api/V2/Request/Model/Account/UpdateRequest.php create mode 100644 resources/assets/v2/api/v2/model/account/put.js create mode 100644 resources/assets/v2/support/editable/GenericEditor.js diff --git a/app/Api/V2/Controllers/Model/Account/UpdateController.php b/app/Api/V2/Controllers/Model/Account/UpdateController.php new file mode 100644 index 0000000000..877977f3d9 --- /dev/null +++ b/app/Api/V2/Controllers/Model/Account/UpdateController.php @@ -0,0 +1,81 @@ +middleware( + function ($request, $next) { + $this->repository = app(AccountRepositoryInterface::class); + // new way of user group validation + $userGroup = $this->validateUserGroup($request); + if (null !== $userGroup) { + $this->repository->setUserGroup($userGroup); + } + + return $next($request); + } + ); + } + + /** + * TODO this endpoint is not yet reachable. + */ + public function update(UpdateRequest $request, Account $account): JsonResponse + { + app('log')->debug(sprintf('Now in %s', __METHOD__)); + $data = $request->getUpdateData(); + $data['type'] = config('firefly.shortNamesByFullName.'.$account->accountType->type); + $account = $this->repository->update($account, $data); + $account->refresh(); + app('preferences')->mark(); + + $transformer = new AccountTransformer(); + $transformer->setParameters($this->parameters); + + return response() + ->api($this->jsonApiObject('accounts', $account, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } + + +} diff --git a/app/Api/V2/Request/Model/Account/UpdateRequest.php b/app/Api/V2/Request/Model/Account/UpdateRequest.php new file mode 100644 index 0000000000..6576f47829 --- /dev/null +++ b/app/Api/V2/Request/Model/Account/UpdateRequest.php @@ -0,0 +1,119 @@ + ['name', 'convertString'], + 'active' => ['active', 'boolean'], + 'include_net_worth' => ['include_net_worth', 'boolean'], + 'account_type_name' => ['type', 'convertString'], + 'virtual_balance' => ['virtual_balance', 'convertString'], + 'iban' => ['iban', 'convertString'], + 'BIC' => ['bic', 'convertString'], + 'account_number' => ['account_number', 'convertString'], + 'account_role' => ['account_role', 'convertString'], + 'liability_type' => ['liability_type', 'convertString'], + 'opening_balance' => ['opening_balance', 'convertString'], + 'opening_balance_date' => ['opening_balance_date', 'convertDateTime'], + 'cc_type' => ['credit_card_type', 'convertString'], + 'cc_monthly_payment_date' => ['monthly_payment_date', 'convertDateTime'], + 'notes' => ['notes', 'stringWithNewlines'], + 'interest' => ['interest', 'convertString'], + 'interest_period' => ['interest_period', 'convertString'], + 'order' => ['order', 'convertInteger'], + 'currency_id' => ['currency_id', 'convertInteger'], + 'currency_code' => ['currency_code', 'convertString'], + 'liability_direction' => ['liability_direction', 'convertString'], + 'liability_amount' => ['liability_amount', 'convertString'], + 'liability_start_date' => ['liability_start_date', 'date'], + ]; + $data = $this->getAllData($fields); + + return $this->appendLocationData($data, null); + } + + /** + * TODO is a duplicate of the v1 UpdateRequest method. + * + * The rules that the incoming request must be matched against. + */ + public function rules(): array + { + /** @var Account $account */ + $account = $this->route()->parameter('account'); + $accountRoles = implode(',', config('firefly.accountRoles')); + $types = implode(',', array_keys(config('firefly.subTitlesByIdentifier'))); + $ccPaymentTypes = implode(',', array_keys(config('firefly.ccTypes'))); + + $rules = [ + 'name' => sprintf('min:1|max:1024|uniqueAccountForUser:%d', $account->id), + 'type' => sprintf('in:%s', $types), + 'iban' => ['iban', 'nullable', new UniqueIban($account, $this->convertString('type'))], + 'bic' => 'bic|nullable', + 'account_number' => ['min:1', 'max:255', 'nullable', new UniqueAccountNumber($account, $this->convertString('type'))], + 'opening_balance' => 'numeric|required_with:opening_balance_date|nullable', + 'opening_balance_date' => 'date|required_with:opening_balance|nullable', + 'virtual_balance' => 'numeric|nullable', + 'order' => 'numeric|nullable', + 'currency_id' => 'numeric|exists:transaction_currencies,id', + 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code', + 'active' => [new IsBoolean()], + 'include_net_worth' => [new IsBoolean()], + 'account_role' => sprintf('in:%s|nullable|required_if:type,asset', $accountRoles), + 'credit_card_type' => sprintf('in:%s|nullable|required_if:account_role,ccAsset', $ccPaymentTypes), + 'monthly_payment_date' => 'date|nullable|required_if:account_role,ccAsset|required_if:credit_card_type,monthlyFull', + 'liability_type' => 'required_if:type,liability|in:loan,debt,mortgage', + 'liability_direction' => 'required_if:type,liability|in:credit,debit', + 'interest' => 'required_if:type,liability|min:0|max:100|numeric', + 'interest_period' => 'required_if:type,liability|in:daily,monthly,yearly', + 'notes' => 'min:0|max:32768', + ]; + + return Location::requestRules($rules); + } + + +} diff --git a/app/Repositories/UserGroups/Account/AccountRepository.php b/app/Repositories/UserGroups/Account/AccountRepository.php index 8e0c53cc41..cecf5cdb60 100644 --- a/app/Repositories/UserGroups/Account/AccountRepository.php +++ b/app/Repositories/UserGroups/Account/AccountRepository.php @@ -28,6 +28,7 @@ use FireflyIII\Models\Account; use FireflyIII\Models\AccountMeta; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Services\Internal\Update\AccountUpdateService; use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Support\Collection; @@ -291,4 +292,12 @@ class AccountRepository implements AccountRepositoryInterface return $dbQuery->take($limit)->get(['accounts.*']); } + + #[\Override] public function update(Account $account, array $data): Account + { + /** @var AccountUpdateService $service */ + $service = app(AccountUpdateService::class); + + return $service->update($account, $data); + } } diff --git a/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php b/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php index 9a70b42795..0f007432f1 100644 --- a/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php +++ b/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php @@ -72,5 +72,7 @@ interface AccountRepositoryInterface public function setUser(User $user): void; + public function update(Account $account,array $data): Account; + public function setUserGroup(UserGroup $userGroup): void; } diff --git a/resources/assets/v2/api/v2/model/account/put.js b/resources/assets/v2/api/v2/model/account/put.js new file mode 100644 index 0000000000..d2ac8a8310 --- /dev/null +++ b/resources/assets/v2/api/v2/model/account/put.js @@ -0,0 +1,36 @@ +/* + * list.js + * 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 . + */ + +import {api} from "../../../../boot/axios"; +import format from "date-fns/format"; + +export default class Put { + + /** + * + * @param identifier + * @param params + * @returns {Promise>} + */ + put(identifier, params) { + return api.put('/api/v2/accounts/' + identifier, params); + } + +} diff --git a/resources/assets/v2/pages/accounts/index.js b/resources/assets/v2/pages/accounts/index.js index 3bdd2a7ae0..792291bb81 100644 --- a/resources/assets/v2/pages/accounts/index.js +++ b/resources/assets/v2/pages/accounts/index.js @@ -28,6 +28,8 @@ import '@ag-grid-community/styles/ag-grid.css'; import '@ag-grid-community/styles/ag-theme-alpine.css'; import '../../css/grid-ff3-theme.css'; import Get from "../../api/v2/model/account/get.js"; +import GenericEditor from "../../support/editable/GenericEditor.js"; +import Put from "../../api/v2/model/account/put.js"; // set type from URL const urlParts = window.location.href.split('/'); @@ -51,6 +53,7 @@ let index = function () { enabled: true }, }, + editors: {}, sortingColumn: '', sortDirection: '', accounts: [], @@ -75,7 +78,36 @@ let index = function () { this.notifications.wait.text = i18next.t('firefly.wait_loading_data') this.loadAccounts(); }, - + submitInlineEdit(e) { + e.preventDefault(); + const newTarget = e.currentTarget; + const index = newTarget.dataset.index; + const newValue = document.querySelectorAll('[data-index="'+index+'input"]')[0].value ?? ''; + if('' === newValue) { + return; + } + // submit the field in an update thing? + const fieldName = this.editors[index].options.field; + const params = {}; + params[fieldName] = newValue; + console.log(params); + console.log('New value is ' + newValue + ' for account #' + this.editors[index].options.id); + (new Put()).put(this.editors[index].options.id, params); + }, + cancelInlineEdit(e) { + const newTarget = e.currentTarget; + const index = newTarget.dataset.index; + this.editors[index].cancel(); + }, + triggerEdit(e) { + const target = e.currentTarget; + const index = target.dataset.index; + // get parent: + this.editors[index] = new GenericEditor(); + this.editors[index].setElement(target); + this.editors[index].init(); + this.editors[index].replace(); + }, loadAccounts() { this.notifications.wait.show = true; this.notifications.wait.text = i18next.t('firefly.wait_loading_data') @@ -100,13 +132,13 @@ let index = function () { currency_code: current.attributes.currency_code, native_current_balance: current.attributes.native_current_balance, native_currency_code: current.attributes.native_currency_code, - last_activity: null === current.attributes.last_activity ? '' : format(new Date(current.attributes.last_activity),'P'), + last_activity: null === current.attributes.last_activity ? '' : format(new Date(current.attributes.last_activity), 'P'), }; this.accounts.push(account); } } this.notifications.wait.show = false; - + // add click trigger thing. }); }, } diff --git a/resources/assets/v2/sass/app.scss b/resources/assets/v2/sass/app.scss index 06a1b91471..6fbdb47374 100644 --- a/resources/assets/v2/sass/app.scss +++ b/resources/assets/v2/sass/app.scss @@ -22,7 +22,7 @@ $color-mode-type: media-query; $link-decoration: none !default; $font-family-sans-serif: "Roboto", sans-serif; -$danger: #CD5029 !default; +$danger: #CD5029 !default; $primary: #1E6581 !default; $success: #64B624 !default; @@ -43,7 +43,13 @@ $success: #64B624 !default; // @import "~bootstrap-sass/assets/stylesheets/bootstrap"; - +// hover buttons +.hidden-edit-button { + cursor: pointer; +} +td:not(:hover) .hidden-edit-button { + visibility: hidden; +} diff --git a/resources/assets/v2/support/editable/GenericEditor.js b/resources/assets/v2/support/editable/GenericEditor.js new file mode 100644 index 0000000000..0c3f5bc893 --- /dev/null +++ b/resources/assets/v2/support/editable/GenericEditor.js @@ -0,0 +1,107 @@ +/* + * GenericEditor.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/. + */ + +export default class GenericEditor { + setElement(element) { + console.log('GenericEditor.setElement()', element); + this.element = element; + this.parent = element.parentElement; + this.options = {}; + } + + init() { + // grab some options from element itself: + this.options.type = this.element.dataset.type; + this.options.id = this.element.dataset.id; + this.options.value = this.element.dataset.value; + this.options.index = this.element.dataset.index; + this.options.model = this.element.dataset.model; + this.options.field = this.element.dataset.field; + //this.options.field = this.element.dataset.type; + console.log('GenericEditor['+this.options.index+'].init()'); + } + + replace() { + console.log('GenericEditor['+this.options.index+'].replace()'); + // save old HTML in data field (does that work, is it safe?) + this.options.original = this.element.parentElement.innerHTML; + if (this.options.type === 'text') { + this.replaceText(); + } + } + + replaceText() { + console.log('GenericEditor['+this.options.index+'].replaceText()'); + let html = this.formStart() + this.rowStart(); + + // input field: + html += this.columnStart('7') + this.label() + this.textField() + this.closeDiv(); + + // add submit button + html += this.columnStart('5') + this.buttonGroup() + this.closeDiv(); + + // close column and form: + html += this.closeDiv() + this.closeForm(); + this.element.parentElement.innerHTML = html; + } + textField() { + return ''; + } + closeDiv() { + return ''; + } + closeForm() { + return ''; + } + + formStart() { + return '
'; + } + + rowStart() { + return '
'; + } + + columnStart(param) { + if ('' === param) { + return '
'; + } + return '
'; + } + label() { + return ''; + } + + buttonGroup() { + return '
'+ + ''+ + '' + + '
'; + } + cancel() { + console.log('GenericEditor['+this.options.index+'].cancel()'); + console.log(this.element); + console.log(this.parent); + this.parent.innerHTML = this.options.original; + } + submitInlineEdit(e) { + console.log('Submit?'); + } +} diff --git a/resources/views/v2/accounts/index.blade.php b/resources/views/v2/accounts/index.blade.php index befa1f6387..fdba25a06d 100644 --- a/resources/views/v2/accounts/index.blade.php +++ b/resources/views/v2/accounts/index.blade.php @@ -99,6 +99,7 @@ + @@ -147,6 +148,8 @@
+ + @endsection @section('scripts') @vite(['resources/assets/v2/pages/accounts/index.js']) diff --git a/routes/api.php b/routes/api.php index d41fae7d9e..223ca2a969 100644 --- a/routes/api.php +++ b/routes/api.php @@ -106,6 +106,7 @@ Route::group( static function (): void { Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']); Route::get('{account}', ['uses' => 'ShowController@show', 'as' => 'show']); + Route::put('{account}', ['uses' => 'UpdateController@update', 'as' => 'update']); } );