diff --git a/app/Api/V2/Controllers/Controller.php b/app/Api/V2/Controllers/Controller.php index 9f5e4dfd49..02631c38cd 100644 --- a/app/Api/V2/Controllers/Controller.php +++ b/app/Api/V2/Controllers/Controller.php @@ -157,6 +157,9 @@ class Controller extends BaseController { $manager = new Manager(); $baseUrl = request()->getSchemeAndHttpHost().'/api/v2'; + + // TODO add stuff to path? + $manager->setSerializer(new JsonApiSerializer($baseUrl)); $objects = $paginator->getCollection(); diff --git a/app/Api/V2/Controllers/Model/Account/IndexController.php b/app/Api/V2/Controllers/Model/Account/IndexController.php index 2994092bfd..883bd5ddd5 100644 --- a/app/Api/V2/Controllers/Model/Account/IndexController.php +++ b/app/Api/V2/Controllers/Model/Account/IndexController.php @@ -33,7 +33,7 @@ use Illuminate\Pagination\LengthAwarePaginator; class IndexController extends Controller { - public const string RESOURCE_KEY = 'accounts'; + public const string RESOURCE_KEY = 'accounts'; private AccountRepositoryInterface $repository; protected array $acceptedRoles = [UserRoleEnum::READ_ONLY, UserRoleEnum::MANAGE_TRANSACTIONS]; @@ -48,7 +48,7 @@ class IndexController extends Controller function ($request, $next) { $this->repository = app(AccountRepositoryInterface::class); // new way of user group validation - $userGroup = $this->validateUserGroup($request); + $userGroup = $this->validateUserGroup($request); $this->repository->setUserGroup($userGroup); return $next($request); @@ -57,26 +57,28 @@ class IndexController extends Controller } /** + * TODO the sort instructions need proper repeatable documentation. * TODO see autocomplete/account controller for list. */ public function index(IndexRequest $request): JsonResponse { $this->repository->resetAccountOrder(); - $types = $request->getAccountTypes(); - $instructions = $request->getSortInstructions('accounts'); - $accounts = $this->repository->getAccountsByType($types, $instructions); - $pageSize = $this->parameters->get('limit'); - $count = $accounts->count(); - $accounts = $accounts->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); - $paginator = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page')); - $transformer = new AccountTransformer(); + $types = $request->getAccountTypes(); + $sorting = $request->getSortInstructions('accounts'); + $filters = $request->getFilterInstructions('accounts'); + $accounts = $this->repository->getAccountsByType($types, $sorting, $filters); + $pageSize = $this->parameters->get('limit'); + $count = $accounts->count(); + $accounts = $accounts->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + $paginator = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page')); + $transformer = new AccountTransformer(); - $this->parameters->set('sort', $instructions); + $this->parameters->set('sort', $sorting); + $this->parameters->set('filters', $filters); $transformer->setParameters($this->parameters); // give params to transformer return response() ->json($this->jsonApiList('accounts', $paginator, $transformer)) - ->header('Content-Type', self::CONTENT_TYPE) - ; + ->header('Content-Type', self::CONTENT_TYPE); } } diff --git a/app/Api/V2/Request/Model/Account/IndexRequest.php b/app/Api/V2/Request/Model/Account/IndexRequest.php index 6597a5f3ba..b64b7cd9ac 100644 --- a/app/Api/V2/Request/Model/Account/IndexRequest.php +++ b/app/Api/V2/Request/Model/Account/IndexRequest.php @@ -27,6 +27,7 @@ use Carbon\Carbon; use FireflyIII\Support\Http\Api\AccountFilter; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; +use FireflyIII\Support\Request\GetFilterInstructions; use FireflyIII\Support\Request\GetSortInstructions; use Illuminate\Foundation\Http\FormRequest; @@ -41,6 +42,8 @@ class IndexRequest extends FormRequest use ChecksLogin; use ConvertsDataTypes; use GetSortInstructions; + use GetFilterInstructions; + public function getAccountTypes(): array { diff --git a/app/Repositories/UserGroups/Account/AccountRepository.php b/app/Repositories/UserGroups/Account/AccountRepository.php index 0e85b8295b..2da67f1629 100644 --- a/app/Repositories/UserGroups/Account/AccountRepository.php +++ b/app/Repositories/UserGroups/Account/AccountRepository.php @@ -240,7 +240,7 @@ class AccountRepository implements AccountRepositoryInterface } } - public function getAccountsByType(array $types, ?array $sort = []): Collection + public function getAccountsByType(array $types, ?array $sort = [], ?array $filters = []): Collection { $sortable = ['name', 'active']; // TODO yes this is a duplicate array. $res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types); @@ -249,6 +249,19 @@ class AccountRepository implements AccountRepositoryInterface $query->accountTypeIn($types); } + // process filters + // TODO this should be repeatable, it feels like a hack when you do it here. + foreach($filters as $column => $value) { + // filter on NULL values + if(null === $value) { + continue; + } + if ('active' === $column) { + $query->where('accounts.active', $value); + } + } + + // add sort parameters. At this point they're filtered to allowed fields to sort by: $hasActiveColumn = array_key_exists('active', $sort); if (count($sort) > 0) { diff --git a/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php b/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php index 1700e09f13..674843687f 100644 --- a/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php +++ b/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php @@ -55,7 +55,7 @@ interface AccountRepositoryInterface public function getAccountsById(array $accountIds): Collection; - public function getAccountsByType(array $types, ?array $sort = []): Collection; + public function getAccountsByType(array $types, ?array $sort = [], ?array $filters = []): Collection; /** * Used in the infinite accounts list. diff --git a/app/Support/Request/GetFilterInstructions.php b/app/Support/Request/GetFilterInstructions.php new file mode 100644 index 0000000000..fc432bbe89 --- /dev/null +++ b/app/Support/Request/GetFilterInstructions.php @@ -0,0 +1,69 @@ +get('filters', []); + $result = []; + if (0 === count($set)) { + return []; + } + foreach ($set as $info) { + $column = $info['column'] ?? 'NOPE'; + $filterValue = (string) ($info['filter'] ?? self::INVALID_FILTER); + if (false === in_array($column, $allowed, true)) { + // skip invalid column + continue; + } + $filterType = $config[$column] ?? false; + switch ($filterType) { + default: + die(sprintf('Do not support filter type "%s"', $filterType)); + case 'boolean': + $filterValue = $this->booleanInstruction($filterValue); + break; + } + $result[$column] = $filterValue; + } + return $result; + } + + public function booleanInstruction(string $filterValue): ?bool { + if ('true' === $filterValue) { + return true; + } + if ('false' === $filterValue) { + return false; + } + return null; + } + +} diff --git a/config/firefly.php b/config/firefly.php index 2087c4fdc4..e2bd27b962 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -436,7 +436,7 @@ return [ 'transfers' => 'fa-exchange', ], - 'bindables' => [ + 'bindables' => [ // models 'account' => Account::class, 'attachment' => Attachment::class, @@ -494,7 +494,7 @@ return [ 'userGroupBill' => UserGroupBill::class, 'userGroup' => UserGroup::class, ], - 'rule-actions' => [ + 'rule-actions' => [ 'set_category' => SetCategory::class, 'clear_category' => ClearCategory::class, 'set_budget' => SetBudget::class, @@ -528,7 +528,7 @@ return [ // 'set_foreign_amount' => SetForeignAmount::class, // 'set_foreign_currency' => SetForeignCurrency::class, ], - 'context-rule-actions' => [ + 'context-rule-actions' => [ 'set_category', 'set_budget', 'add_tag', @@ -547,13 +547,13 @@ return [ 'convert_transfer', ], - 'test-triggers' => [ + 'test-triggers' => [ 'limit' => 10, 'range' => 200, ], // expected source types for each transaction type, in order of preference. - 'expected_source_types' => [ + 'expected_source_types' => [ 'source' => [ TransactionTypeModel::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionTypeEnum::DEPOSIT->value => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::REVENUE, AccountType::CASH], @@ -598,7 +598,7 @@ return [ TransactionTypeModel::LIABILITY_CREDIT => [AccountType::LIABILITY_CREDIT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], ], ], - 'allowed_opposing_types' => [ + 'allowed_opposing_types' => [ 'source' => [ AccountType::ASSET => [ AccountType::ASSET, @@ -688,7 +688,7 @@ return [ ], ], // depending on the account type, return the allowed transaction types: - 'allowed_transaction_types' => [ + 'allowed_transaction_types' => [ 'source' => [ AccountType::ASSET => [ TransactionTypeModel::WITHDRAWAL, @@ -757,7 +757,7 @@ return [ ], // having the source + dest will tell you the transaction type. - 'account_to_transaction' => [ + 'account_to_transaction' => [ AccountType::ASSET => [ AccountType::ASSET => TransactionTypeModel::TRANSFER, AccountType::CASH => TransactionTypeModel::WITHDRAWAL, @@ -822,7 +822,7 @@ return [ ], // allowed source -> destination accounts. - 'source_dests' => [ + 'source_dests' => [ TransactionTypeModel::WITHDRAWAL => [ AccountType::ASSET => [AccountType::EXPENSE, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::CASH], AccountType::LOAN => [AccountType::EXPENSE, AccountType::CASH], @@ -861,7 +861,7 @@ return [ ], ], // if you add fields to this array, don't forget to update the export routine (ExportDataGenerator). - 'journal_meta_fields' => [ + 'journal_meta_fields' => [ // sepa 'sepa_cc', 'sepa_ct_op', @@ -895,33 +895,47 @@ return [ 'recurrence_count', 'recurrence_date', ], - 'webhooks' => [ + 'webhooks' => [ 'max_attempts' => env('WEBHOOK_MAX_ATTEMPTS', 3), ], - 'can_have_virtual_amounts' => [AccountType::ASSET], - 'can_have_opening_balance' => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - 'dynamic_creation_allowed' => [ + 'can_have_virtual_amounts' => [AccountType::ASSET], + 'can_have_opening_balance' => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + 'dynamic_creation_allowed' => [ AccountType::EXPENSE, AccountType::REVENUE, AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION, AccountType::LIABILITY_CREDIT, ], - 'valid_asset_fields' => ['account_role', 'account_number', 'currency_id', 'BIC', 'include_net_worth'], - 'valid_cc_fields' => ['account_role', 'cc_monthly_payment_date', 'cc_type', 'account_number', 'currency_id', 'BIC', 'include_net_worth'], - 'valid_account_fields' => ['account_number', 'currency_id', 'BIC', 'interest', 'interest_period', 'include_net_worth', 'liability_direction'], + 'valid_asset_fields' => ['account_role', 'account_number', 'currency_id', 'BIC', 'include_net_worth'], + 'valid_cc_fields' => ['account_role', 'cc_monthly_payment_date', 'cc_type', 'account_number', 'currency_id', 'BIC', 'include_net_worth'], + 'valid_account_fields' => ['account_number', 'currency_id', 'BIC', 'interest', 'interest_period', 'include_net_worth', 'liability_direction'], // dynamic date ranges are as follows: - 'dynamic_date_ranges' => ['last7', 'last30', 'last90', 'last365', 'MTD', 'QTD', 'YTD'], + 'dynamic_date_ranges' => ['last7', 'last30', 'last90', 'last365', 'MTD', 'QTD', 'YTD'], // only used in v1 - 'allowed_sort_parameters' => ['order', 'name', 'iban'], + 'allowed_sort_parameters' => ['order', 'name', 'iban'], // preselected account lists possibilities: - 'preselected_accounts' => ['all', 'assets', 'liabilities'], + 'preselected_accounts' => ['all', 'assets', 'liabilities'], - // allowed sort columns for API's - 'sorting' => [ + // allowed filters (search) for APIs + 'filters' => [ + 'allowed' => [ + 'accounts' => [ + 'name' => '*', + 'active' => 'boolean', + 'iban' => 'iban', + 'balance' => 'numeric', + 'last_activity' => 'date', + 'balance_difference' => 'numeric', + ], + ], + ], + + // allowed sort columns for APIs + 'sorting' => [ 'allowed' => [ 'transactions' => ['description', 'amount'], 'accounts' => ['name', 'active', 'iban', 'balance', 'last_activity', 'balance_difference'], diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss deleted file mode 100644 index b94b9657e1..0000000000 --- a/resources/assets/sass/_variables.scss +++ /dev/null @@ -1,58 +0,0 @@ -/*! - * _variables.scss - * 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 . - */ - -/* TODO REMOVE ME */ -// Body -$body-bg: #f5f8fa; - -// Borders -$laravel-border-color: darken($body-bg, 10%); -$list-group-border: $laravel-border-color; -$navbar-default-border: $laravel-border-color; -$panel-default-border: $laravel-border-color; -$panel-inner-border: $laravel-border-color; - -// Brands -$brand-primary: #3097D1; -$brand-info: #8eb4cb; -$brand-success: #2ab27b; -$brand-warning: #cbb956; -$brand-danger: #bf5329; - -// Typography -$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/"; -$font-family-sans-serif: "Raleway", sans-serif; -$font-size-base: 14px; -$line-height-base: 1.6; -$text-color: #636b6f; - -// Navbar -$navbar-default-bg: #fff; - -// Buttons -$btn-default-color: $text-color; - -// Inputs -$input-border: lighten($text-color, 40%); -$input-border-focus: lighten($brand-primary, 25%); -$input-color-placeholder: lighten($text-color, 30%); - -// Panels -$panel-default-heading-bg: #fff; diff --git a/resources/assets/v2/src/api/v2/model/account/get.js b/resources/assets/v2/src/api/v2/model/account/get.js index f2e63c4152..8c313518f2 100644 --- a/resources/assets/v2/src/api/v2/model/account/get.js +++ b/resources/assets/v2/src/api/v2/model/account/get.js @@ -20,6 +20,7 @@ import {api} from "../../../../boot/axios"; import format from "date-fns/format"; +import {getCacheKey} from "../../../../support/get-cache-key.js"; export default class Get { @@ -39,7 +40,24 @@ export default class Get { * @returns {Promise>} */ index(params) { - return api.get('/api/v2/accounts', {params: params}); + // first, check API in some consistent manner. + // then, load if necessary. + const cacheKey = getCacheKey('/api/v2/accounts', params); + const cacheValid = window.store.get('cacheValid'); + let cachedData = window.store.get(cacheKey); + + if (cacheValid && typeof cachedData !== 'undefined') { + console.log('Cache is valid, return cache.'); + return Promise.resolve(cachedData); + } + + // if not, store in cache and then return res. + + return api.get('/api/v2/accounts', {params: params}).then(response => { + console.log('Cache is invalid, return fresh.'); + window.store.set(cacheKey, response.data); + return Promise.resolve({data: response.data.data, meta: response.data.meta}); + }); } /** diff --git a/resources/assets/v2/src/pages/accounts/index.js b/resources/assets/v2/src/pages/accounts/index.js index 3444f23537..dfa7dd42f5 100644 --- a/resources/assets/v2/src/pages/accounts/index.js +++ b/resources/assets/v2/src/pages/accounts/index.js @@ -24,16 +24,15 @@ import i18next from "i18next"; import {format} from "date-fns"; import formatMoney from "../../util/format-money.js"; -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 Put from "../../api/v2/model/account/put.js"; import AccountRenderer from "../../support/renderers/AccountRenderer.js"; import {showInternalsButton} from "../../support/page-settings/show-internals-button.js"; import {showWizardButton} from "../../support/page-settings/show-wizard-button.js"; -import {getVariable} from "../../store/get-variable.js"; import {setVariable} from "../../store/set-variable.js"; +import {getVariables} from "../../store/get-variables.js"; +import pageNavigation from "../../support/page-navigation.js"; +import {getCacheKey} from "../../support/get-cache-key.js"; // set type from URL @@ -43,6 +42,7 @@ const type = urlParts[urlParts.length - 1]; let sortingColumn = ''; let sortDirection = ''; +let page = 1; // get sort parameters const params = new Proxy(new URLSearchParams(window.location.search), { @@ -50,11 +50,15 @@ const params = new Proxy(new URLSearchParams(window.location.search), { }); sortingColumn = params.column ?? ''; sortDirection = params.direction ?? ''; +page = parseInt(params.page ?? 1); showInternalsButton(); showWizardButton(); +// TODO currency conversion +// TODO page cleanup and recycle for transaction lists. + let index = function () { return { // notifications @@ -70,11 +74,13 @@ let index = function () { }, totalPages: 1, page: 1, + pageUrl: '', filters: { - active: 'both', + active: null, name: null, }, pageOptions: { + isLoading: true, groupedAccounts: true, sortingColumn: sortingColumn, sortDirection: sortDirection, @@ -138,29 +144,46 @@ let index = function () { }, editors: {}, accounts: [], - + goToPage(page) { + this.page = page; + this.loadAccounts(); + }, accountRole(roleName) { return i18next.t('firefly.account_role_' + roleName); }, + getPreferenceKey(name) { + return 'acc_index_' + type + '_' + name; + }, + pageNavigation() { + return pageNavigation(this.totalPages, this.page, this.generatePageUrl(false)); + }, sort(column) { + this.page =1; this.pageOptions.sortingColumn = column; this.pageOptions.sortDirection = this.pageOptions.sortDirection === 'asc' ? 'desc' : 'asc'; - const url = './accounts/' + type + '?column=' + column + '&direction=' + this.pageOptions.sortDirection; - window.history.pushState({}, "", url); + this.updatePageUrl(); // get sort column - // TODO variable name in better place - const columnKey = 'acc_index_' + type + '_sc'; - const directionKey = 'acc_index_' + type + '_sd'; - - setVariable(columnKey, this.pageOptions.sortingColumn); - setVariable(directionKey, this.pageOptions.sortDirection); + setVariable(this.getPreferenceKey('sc'), this.pageOptions.sortingColumn); + setVariable(this.getPreferenceKey('sd'), this.pageOptions.sortDirection); this.loadAccounts(); return false; }, + updatePageUrl() { + this.pageUrl = this.generatePageUrl(true); + + window.history.pushState({}, "", this.pageUrl); + }, + generatePageUrl(includePageNr) { + let url = './accounts/' + type + '?column=' + this.pageOptions.sortingColumn + '&direction=' + this.pageOptions.sortDirection + '&page='; + if(includePageNr) { + return url + this.page + } + return url; + }, formatMoney(amount, currencyCode) { return formatMoney(amount, currencyCode); @@ -177,56 +200,58 @@ let index = function () { } } console.log('New settings', newSettings); - setVariable('acc_index_' + type + '_columns', newSettings); + setVariable(this.getPreferenceKey('columns'), newSettings); }, init() { + this.pageOptions.isLoading = true; this.notifications.wait.show = true; + this.page = page; this.notifications.wait.text = i18next.t('firefly.wait_loading_data'); - // get column preference - // TODO key in better variable - const key = 'acc_index_' + type + '_columns'; - const defaultValue = {"drag_and_drop": false}; - - // get sort column - const columnKey = 'acc_index_' + type + '_sc'; - const columnDefault = ''; - - // get sort direction - const directionKey = 'acc_index_' + type + '_sd'; - const directionDefault = ''; - - - getVariable(key, defaultValue).then((response) => { - for (let k in response) { - if (response.hasOwnProperty(k) && this.tableColumns.hasOwnProperty(k)) { - this.tableColumns[k].enabled = response[k] ?? true; + // start by collecting all preferences, create + put in the local store. + getVariables([ + {name: this.getPreferenceKey('columns'), default: {"drag_and_drop": false}}, + {name: this.getPreferenceKey('sc'), default: ''}, + {name: this.getPreferenceKey('sd'), default: ''}, + {name: this.getPreferenceKey('filters'), default: this.filters}, + ]).then((res) => { + // process columns: + for (let k in res[0]) { + if (res[0].hasOwnProperty(k) && this.tableColumns.hasOwnProperty(k)) { + this.tableColumns[k].enabled = res[0][k] ?? true; } } - }). - // get sorting preference, and overrule it if is not "" twice - then(() => { - return getVariable(columnKey, columnDefault).then((response) => { - console.log('Sorting column is "' + response + '"'); - this.pageOptions.sortingColumn = '' === this.pageOptions.sortingColumn ? response : this.pageOptions.sortingColumn; - }) - }) - . - // get sorting preference, and overrule it if is not "" twice - then(() => { - return getVariable(directionKey, directionDefault).then((response) => { - console.log('Sorting direction is "' + response + '"'); - this.pageOptions.sortDirection = '' === this.pageOptions.sortDirection ? response : this.pageOptions.sortDirection; - }) - }). + // process sorting column: + this.pageOptions.sortingColumn = '' === this.pageOptions.sortingColumn ? res[1] : this.pageOptions.sortingColumn; + // process sort direction + this.pageOptions.sortDirection = '' === this.pageOptions.sortDirection ? res[2] : this.pageOptions.sortDirection; + + // filters + for(let k in res[3]) { + if (res[3].hasOwnProperty(k) && this.filters.hasOwnProperty(k)) { + this.filters[k] = res[3][k]; + } + } - then(() => { this.loadAccounts(); }); - + }, + saveActiveFilter(e) { + this.page = 1; + if('both' === e.currentTarget.value) { + this.filters.active = null; + } + if('active' === e.currentTarget.value) { + this.filters.active = true; + } + if('inactive' === e.currentTarget.value) { + this.filters.active = false; + } + setVariable(this.getPreferenceKey('filters'), this.filters); + this.loadAccounts(); }, renderObjectValue(field, account) { let renderer = new AccountRenderer(); @@ -270,36 +295,47 @@ let index = function () { this.accounts[index].nameEditorVisible = true; }, loadAccounts() { - + this.pageOptions.isLoading = true; // sort instructions const sorting = [{column: this.pageOptions.sortingColumn, direction: this.pageOptions.sortDirection}]; + // filter instructions + let filters = []; + for(let k in this.filters) { + if(this.filters.hasOwnProperty(k) && null !== this.filters[k]) { + filters.push({column: k, filter: this.filters[k]}); + } + } + // get start and end from the store: const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); + const today = new Date(); let params = { sorting: sorting, + filters: filters, + today: today, type: type, page: this.page, start: start, end: end }; - if(!this.tableColumns.balance_difference.enabled){ + if (!this.tableColumns.balance_difference.enabled) { delete params.start; delete params.end; } + // check if cache is present: - this.notifications.wait.show = true; - this.notifications.wait.text = i18next.t('firefly.wait_loading_data') this.accounts = []; // one page only.o (new Get()).index(params).then(response => { - for (let i = 0; i < response.data.data.length; i++) { - if (response.data.data.hasOwnProperty(i)) { - let current = response.data.data[i]; + this.totalPages = response.meta.pagination.total_pages; + for (let i = 0; i < response.data.length; i++) { + if (response.data.hasOwnProperty(i)) { + let current = response.data[i]; let account = { id: parseInt(current.id), active: current.attributes.active, @@ -317,11 +353,11 @@ let index = function () { balance_difference: current.attributes.balance_difference, native_balance_difference: current.attributes.native_balance_difference }; - console.log(current.attributes.balance_difference); this.accounts.push(account); } } this.notifications.wait.show = false; + this.pageOptions.isLoading = false; // add click trigger thing. }); }, diff --git a/resources/assets/v2/src/pages/administrations/edit.js b/resources/assets/v2/src/pages/administrations/edit.js index ed944eaf7f..1cca799b6f 100644 --- a/resources/assets/v2/src/pages/administrations/edit.js +++ b/resources/assets/v2/src/pages/administrations/edit.js @@ -63,7 +63,6 @@ let administrations = function () { pageProperties: {}, submitForm() { - console.log('submitForm'); (new Put()).put({title: this.title}, {id: this.id}).then(response => { if (this.formStates.returnHereButton) { this.notifications.success.show = true; diff --git a/resources/assets/v2/src/pages/dashboard/accounts.js b/resources/assets/v2/src/pages/dashboard/accounts.js index 2849004aa7..3bdd8a3076 100644 --- a/resources/assets/v2/src/pages/dashboard/accounts.js +++ b/resources/assets/v2/src/pages/dashboard/accounts.js @@ -46,16 +46,18 @@ export default () => ({ this.autoConversion = !this.autoConversion; setVariable('autoConversion', this.autoConversion); }, + localCacheKey(type) { + return 'ds_accounts_' + type; + }, getFreshData() { const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); - const chartCacheKey = getCacheKey('dashboard-accounts-chart', start, end) + const chartCacheKey = getCacheKey(this.localCacheKey('chart'), {start: start, end: end}) const cacheValid = window.store.get('cacheValid'); let cachedData = window.store.get(chartCacheKey); if (cacheValid && typeof cachedData !== 'undefined') { - console.log(cachedData); this.drawChart(this.generateOptions(cachedData)); this.loading = false; return; @@ -65,7 +67,6 @@ export default () => ({ this.chartData = response.data; // cache generated options: window.store.set(chartCacheKey, response.data); - console.log(response.data); this.drawChart(this.generateOptions(this.chartData)); this.loading = false; }); @@ -168,7 +169,7 @@ export default () => ({ } const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); - const accountCacheKey = getCacheKey('dashboard-accounts-data', start, end); + const accountCacheKey = getCacheKey(this.localCacheKey('data'), {start: start, end: end}); const cacheValid = window.store.get('cacheValid'); let cachedData = window.store.get(accountCacheKey); @@ -221,7 +222,6 @@ export default () => ({ // if transfer and source is this account, multiply again if('transfer' === currentTransaction.type && parseInt(currentTransaction.source_id) === accountId) { // - console.log('transfer', parseInt(currentTransaction.source_id), accountId); nativeAmountRaw = nativeAmountRaw * -1; amountRaw = amountRaw * -1; } diff --git a/resources/assets/v2/src/pages/dashboard/boxes.js b/resources/assets/v2/src/pages/dashboard/boxes.js index cdc98ec02f..1bcc66497a 100644 --- a/resources/assets/v2/src/pages/dashboard/boxes.js +++ b/resources/assets/v2/src/pages/dashboard/boxes.js @@ -38,7 +38,8 @@ export default () => ({ getFreshData() { const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); - const boxesCacheKey = getCacheKey('dashboard-boxes-data', start, end); + // TODO cache key is hard coded, problem? + const boxesCacheKey = getCacheKey('ds_boxes_data', {start: start, end: end}); cleanupCache(); const cacheValid = window.store.get('cacheValid'); @@ -208,6 +209,7 @@ export default () => ({ // Getter init() { // console.log('boxes init'); + // TODO can be replaced by "getVariables" Promise.all([getVariable('viewRange'), getVariable('autoConversion', false)]).then((values) => { // console.log('boxes after promises'); afterPromises = true; diff --git a/resources/assets/v2/src/pages/dashboard/budgets.js b/resources/assets/v2/src/pages/dashboard/budgets.js index 903fb9549c..fe7dc039af 100644 --- a/resources/assets/v2/src/pages/dashboard/budgets.js +++ b/resources/assets/v2/src/pages/dashboard/budgets.js @@ -59,7 +59,7 @@ export default () => ({ getFreshData() { const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); - const cacheKey = getCacheKey('dashboard-budgets-chart', start, end); + const cacheKey = getCacheKey('ds_bdg_chart', {start: start, end: end}); const cacheValid = window.store.get('cacheValid'); let cachedData = window.store.get(cacheKey); diff --git a/resources/assets/v2/src/pages/dashboard/categories.js b/resources/assets/v2/src/pages/dashboard/categories.js index e8a5598ba5..8f68e28b38 100644 --- a/resources/assets/v2/src/pages/dashboard/categories.js +++ b/resources/assets/v2/src/pages/dashboard/categories.js @@ -147,7 +147,7 @@ export default () => ({ getFreshData() { const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); - const cacheKey = getCacheKey('dashboard-categories-chart', start, end); + const cacheKey = getCacheKey('ds_ct_chart', {start: start, end: end}); const cacheValid = window.store.get('cacheValid'); let cachedData = window.store.get(cacheKey); diff --git a/resources/assets/v2/src/pages/dashboard/piggies.js b/resources/assets/v2/src/pages/dashboard/piggies.js index 9fd1392769..037932f9f2 100644 --- a/resources/assets/v2/src/pages/dashboard/piggies.js +++ b/resources/assets/v2/src/pages/dashboard/piggies.js @@ -25,7 +25,7 @@ import i18next from "i18next"; let apiData = {}; let afterPromises = false; -const PIGGY_CACHE_KEY = 'dashboard-piggies-data'; +const PIGGY_CACHE_KEY = 'ds_pg_data'; export default () => ({ loading: false, @@ -36,7 +36,7 @@ export default () => ({ const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); // needs user data. - const cacheKey = getCacheKey(PIGGY_CACHE_KEY, start, end); + const cacheKey = getCacheKey(PIGGY_CACHE_KEY, {start: start, end: end}); const cacheValid = window.store.get('cacheValid'); let cachedData = window.store.get(cacheKey); @@ -58,7 +58,7 @@ export default () => ({ downloadPiggyBanks(params) { const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); - const cacheKey = getCacheKey(PIGGY_CACHE_KEY, start, end); + const cacheKey = getCacheKey(PIGGY_CACHE_KEY, {start: start, end: end}); const getter = new Get(); getter.list(params).then((response) => { apiData = [...apiData, ...response.data.data]; diff --git a/resources/assets/v2/src/pages/dashboard/sankey.js b/resources/assets/v2/src/pages/dashboard/sankey.js index 0f2d841524..40a73e5aaa 100644 --- a/resources/assets/v2/src/pages/dashboard/sankey.js +++ b/resources/assets/v2/src/pages/dashboard/sankey.js @@ -28,7 +28,7 @@ import i18next from "i18next"; Chart.register({SankeyController, Flow}); -const SANKEY_CACHE_KEY = 'dashboard-sankey-data'; +const SANKEY_CACHE_KEY = 'ds_sankey_data'; let currencies = []; let afterPromises = false; let chart = null; @@ -288,7 +288,7 @@ export default () => ({ getFreshData() { const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); - const cacheKey = getCacheKey(SANKEY_CACHE_KEY, start, end); + const cacheKey = getCacheKey(SANKEY_CACHE_KEY, {start: start, end: end}); const cacheValid = window.store.get('cacheValid'); let cachedData = window.store.get(cacheKey); @@ -312,7 +312,7 @@ export default () => ({ downloadTransactions(params) { const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); - const cacheKey = getCacheKey(SANKEY_CACHE_KEY, start, end); + const cacheKey = getCacheKey(SANKEY_CACHE_KEY, {start: start, end: end}); //console.log('Downloading page ' + params.page + '...'); const getter = new Get(); diff --git a/resources/assets/v2/src/pages/dashboard/subscriptions.js b/resources/assets/v2/src/pages/dashboard/subscriptions.js index 6a90e1f2d3..9cca5c9c7e 100644 --- a/resources/assets/v2/src/pages/dashboard/subscriptions.js +++ b/resources/assets/v2/src/pages/dashboard/subscriptions.js @@ -186,7 +186,7 @@ export default () => ({ let end = new Date(window.store.get('end')); const cacheValid = window.store.get('cacheValid'); - let cachedData = window.store.get(getCacheKey('subscriptions-data-dashboard', start, end)); + let cachedData = window.store.get(getCacheKey('ds_sub_data', {start: start, end: end})); if (cacheValid && typeof cachedData !== 'undefined' && false) { console.error('cannot handle yet'); diff --git a/resources/assets/v2/src/store/get-variables.js b/resources/assets/v2/src/store/get-variables.js new file mode 100644 index 0000000000..ecdf586277 --- /dev/null +++ b/resources/assets/v2/src/store/get-variables.js @@ -0,0 +1,47 @@ +/* + * get-variable.js + * 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 . + */ + +import {getVariable} from "./get-variable.js"; + +export function getVariables(preferences) { + let chain = Promise.resolve(); + let allVariables = []; + for (let i = 0; i < preferences.length; i++) { + + let current = preferences[i]; + let name = current.name; + let defaultValue = current.default; + chain = chain.then(() => { + return getVariable(name, defaultValue).then((value) => { + allVariables.push(value); + return Promise.resolve(allVariables); + }); + }); + } + return chain; + +} + +export function parseResponse(name, response) { + let value = response.data.data.attributes.data; + window.store.set(name, value); + return value; +} + diff --git a/resources/assets/v2/src/support/get-cache-key.js b/resources/assets/v2/src/support/get-cache-key.js index 2af34f3062..adedf73b49 100644 --- a/resources/assets/v2/src/support/get-cache-key.js +++ b/resources/assets/v2/src/support/get-cache-key.js @@ -20,12 +20,77 @@ import {format} from "date-fns"; import store from "store"; +//const { createHash } = require('crypto'); -function getCacheKey(string, start, end) { - const localValue = store.get('lastActivity'); - const cacheKey = 'dcx' + format(start, 'yMMdd')+ format(end, 'yMMdd') + string + localValue; - console.log('getCacheKey: ' + cacheKey); - return String(cacheKey); + + +function getCacheKey(string, params) { + const lastActivity = store.get('lastActivity') + let newParams = {lastActivity: lastActivity, key: string}; + + for (const key in params) { + if (params.hasOwnProperty(key)) { + if(params[key] === null || params[key] === undefined) { + newParams[key] = ''; + continue; + } + if(params[key] instanceof Date) { + newParams[key] = format(params[key], 'yMMdd'); + continue; + } + newParams[key] = params[key]; + } + } + return 'dcx_' + md5(JSON.stringify(newParams)).substring(0,12) + lastActivity; } +// Formatted version of a popular md5 implementation +// Original copyright (c) Paul Johnston & Greg Holt. +// The function itself is now 42 lines long. + +function md5(inputString) { + var hc="0123456789abcdef"; + function rh(n) {var j,s="";for(j=0;j<=3;j++) s+=hc.charAt((n>>(j*8+4))&0x0F)+hc.charAt((n>>(j*8))&0x0F);return s;} + function ad(x,y) {var l=(x&0xFFFF)+(y&0xFFFF);var m=(x>>16)+(y>>16)+(l>>16);return (m<<16)|(l&0xFFFF);} + function rl(n,c) {return (n<>>(32-c));} + function cm(q,a,b,x,s,t) {return ad(rl(ad(ad(a,q),ad(x,t)),s),b);} + function ff(a,b,c,d,x,s,t) {return cm((b&c)|((~b)&d),a,b,x,s,t);} + function gg(a,b,c,d,x,s,t) {return cm((b&d)|(c&(~d)),a,b,x,s,t);} + function hh(a,b,c,d,x,s,t) {return cm(b^c^d,a,b,x,s,t);} + function ii(a,b,c,d,x,s,t) {return cm(c^(b|(~d)),a,b,x,s,t);} + function sb(x) { + var i;var nblk=((x.length+8)>>6)+1;var blks=new Array(nblk*16);for(i=0;i>2]|=x.charCodeAt(i)<<((i%4)*8); + blks[i>>2]|=0x80<<((i%4)*8);blks[nblk*16-2]=x.length*8;return blks; + } + var i,x=sb(""+inputString),a=1732584193,b=-271733879,c=-1732584194,d=271733878,olda,oldb,oldc,oldd; + for(i=0;i'+p+''; + } + // href="'+ linkURL+ p + '" + return ''+p+''; + + // return ((p === page) ? "" + p + "" : '' + p + ""); + } + + let page = (matchPage ? matchPage : 1), LINKS_PER_STEP = 5, lastp1 = 1, lastp2 = page, p1 = 1, p2 = page, + c1 = LINKS_PER_STEP + 1, c2 = LINKS_PER_STEP + 1, s1 = "", s2 = "", step = 1, linkHTML = ""; + + while (true) { + if (c1 >= c2) { + s1 += pageLink(p1, matchPage); + lastp1 = p1; + p1 += step; + c1--; + } else { + s2 = pageLink(p2, matchPage) + s2; + lastp2 = p2; + p2 -= step; + c2--; + } + if (c2 === 0) { + step *= 25; + p1 += step - 1; // Round UP to nearest multiple of step + p1 -= (p1 % step); + p2 -= (p2 % step); // Round DOWN to nearest multiple of step + c1 = LINKS_PER_STEP; + c2 = LINKS_PER_STEP; + } + if (p1 > p2) { + linkHTML += s1 + s2; + if ((lastp2 > page) || (page >= lastPage)) break; + lastp1 = page; + lastp2 = lastPage; + p1 = page + 1; + p2 = lastPage; + c1 = LINKS_PER_STEP; + c2 = LINKS_PER_STEP + 1; + s1 = ''; + s2 = ''; + step = 1; + } + } + return linkHTML; +} + +export default function pageNavigation(totalPages, currentPage, navigationURL) { + + totalPages = parseInt(totalPages); + currentPage = parseInt(currentPage); + let html = ''; + html += ''; + if(currentPage > 1) { + html += 'Previous'; + } + if(1 === currentPage) { + html += 'Previous'; + } + html += logarithmicPaginationLinks(totalPages, currentPage, navigationURL); + if(currentPage !== totalPages) { + html += 'Next'; + } + if(currentPage === totalPages) { + html += 'Next'; + } + html += ''; + + return html; +} diff --git a/resources/views/v2/accounts/index.blade.php b/resources/views/v2/accounts/index.blade.php index 0704791ba3..55337ebb70 100644 --- a/resources/views/v2/accounts/index.blade.php +++ b/resources/views/v2/accounts/index.blade.php @@ -37,7 +37,7 @@ - Nav + @@ -97,9 +97,9 @@ Active? + class="fa-solid fa-arrow-down-short-wide"> + class="fa-solid fa-arrow-down-wide-short"> Name @@ -121,16 +121,16 @@ Account number + class="fa-solid fa-arrow-down-a-z"> + class="fa-solid fa-arrow-down-z-a"> Current balance + class="fa-solid fa-arrow-down-9-1"> + class="fa-solid fa-arrow-down-1-9"> Amount due @@ -156,6 +156,11 @@ + + + + + @@ -237,7 +242,7 @@ + x-text="formatMoney(account.native_current_balance, account.currency_code)"> TODO @@ -289,7 +294,7 @@ - Nav + @@ -320,11 +325,11 @@ Active accounts? - - Active accounts only - Inactive accounts only + + Active accounts only + Inactive accounts only - All accounts + All accounts TODO Bla bla bla.