diff --git a/app/JsonApi/V2/Accounts/AccountSchema.php b/app/JsonApi/V2/Accounts/AccountSchema.php index 7a7963f77f..1374ded384 100644 --- a/app/JsonApi/V2/Accounts/AccountSchema.php +++ b/app/JsonApi/V2/Accounts/AccountSchema.php @@ -63,7 +63,7 @@ class AccountSchema extends Schema Attribute::make('current_debt')->sortable(), // dynamic data - Attribute::make('last_activity'), + Attribute::make('last_activity')->sortable(), Attribute::make('balance_difference')->sortable(), // only used for sort. // group diff --git a/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php b/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php index 670899e966..554036b115 100644 --- a/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php +++ b/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php @@ -31,13 +31,13 @@ use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; use FireflyIII\Support\JsonApi\ExpandsQuery; use FireflyIII\Support\JsonApi\FiltersPagination; use FireflyIII\Support\JsonApi\SortsCollection; +use FireflyIII\Support\JsonApi\SortsQueryResults; use FireflyIII\Support\JsonApi\ValidateSortParameters; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\Log; use LaravelJsonApi\Contracts\Pagination\Page; use LaravelJsonApi\Contracts\Store\HasPagination; use LaravelJsonApi\NonEloquent\Capabilities\QueryAll; -use LaravelJsonApi\NonEloquent\Concerns\PaginatesEnumerables; class AccountQuery extends QueryAll implements HasPagination { @@ -48,6 +48,8 @@ class AccountQuery extends QueryAll implements HasPagination use ValidateSortParameters; use CollectsCustomParameters; use AccountFilter; + use SortsQueryResults; + //use PaginatesEnumerables; #[\Override] @@ -59,17 +61,15 @@ class AccountQuery extends QueryAll implements HasPagination public function get(): iterable { Log::debug(__METHOD__); - // collect filters - $filters = $this->queryParameters->filter(); - // collect sort options $sort = $this->queryParameters->sortFields(); // collect pagination based on the page $pagination = $this->filtersPagination($this->queryParameters->page()); + // check if we need all accounts, regardless of pagination // This is necessary when the user wants to sort on specific params. - $needsAll = $this->needsFullDataset('account', $sort); + $needsAll = $this->needsFullDataset(Account::class, $sort); // params that were not recognised, may be my own custom stuff. $otherParams = $this->getOtherParams($this->queryParameters->unrecognisedParameters()); @@ -77,34 +77,56 @@ class AccountQuery extends QueryAll implements HasPagination // start the query $query = $this->userGroup->accounts(); -// if (!$needsAll) { -// Log::debug('Does not need full dataset, will paginate.'); -// $query = $this->addPagination($query, $pagination); -// } - // add sort and filter parameters to the query. $query = $this->addSortParams(Account::class, $query, $sort); - $query = $this->addFilterParams(Account::class, $query, $filters); + $query = $this->addFilterParams(Account::class, $query, $this->queryParameters->filter()); + // collect the result. $collection = $query->get(['accounts.*']); // sort the data after the query, and return it right away. - $sorted = $this->sortCollection(Account::class, $collection, $sort); + $collection = $this->sortCollection(Account::class, $collection, $sort); - // take from the collection the filtered page + page number: - $currentPage = $sorted->skip($pagination['number'] - 1 * $pagination['size'])->take($pagination['size']); + // if the entire collection needs to be enriched and sorted, do so now: + $totalCount = $collection->count(); + Log::debug(sprintf('Total is %d', $totalCount)); + if ($needsAll) { + Log::debug('Needs the entire collection'); + // enrich the entire collection + $enrichment = new AccountEnrichment(); + $enrichment->setStart($otherParams['start'] ?? null); + $enrichment->setEnd($otherParams['end'] ?? null); + $collection = $enrichment->enrich($collection); + + // TODO sort the set based on post-query sort options: + $collection = $this->postQuerySort(Account::class, $collection, $sort); + + // take the current page from the enriched set. + $currentPage = $collection->skip(($pagination['number'] - 1) * $pagination['size'])->take($pagination['size']); + + + } + if (!$needsAll) { + Log::debug('Needs only partial collection'); + // take from the collection the filtered page + page number: + $currentPage = $collection->skip(($pagination['number'] - 1) * $pagination['size'])->take($pagination['size']); + + // enrich only the current page. + $enrichment = new AccountEnrichment(); + $enrichment->setStart($otherParams['start'] ?? null); + $enrichment->setEnd($otherParams['end'] ?? null); + $currentPage = $enrichment->enrich($currentPage); + } + // get current page? + Log::debug(sprintf('Skip %d, take %d', ($pagination['number'] - 1) * $pagination['size'], $pagination['size'])); + //$currentPage = $collection->skip(($pagination['number'] - 1) * $pagination['size'])->take($pagination['size']); + Log::debug(sprintf('New collection size: %d', $currentPage->count())); - // enrich the current page. - $enrichment = new AccountEnrichment(); - $enrichment->setStart($otherParams['start'] ?? null); - $enrichment->setEnd($otherParams['end'] ?? null); - $currentPage = $enrichment->enrich($currentPage); // TODO add filters after the query, if there are filters that cannot be applied to the database // TODO same for sort things. - - return new LengthAwarePaginator($currentPage,$sorted->count(),$pagination['size'],$pagination['number']); + return new LengthAwarePaginator($currentPage, $totalCount, $pagination['size'], $pagination['number']); } /** diff --git a/app/Rules/Account/IsValidAccountType.php b/app/Rules/Account/IsValidAccountType.php index 0bace91250..873f96b64d 100644 --- a/app/Rules/Account/IsValidAccountType.php +++ b/app/Rules/Account/IsValidAccountType.php @@ -37,12 +37,14 @@ class IsValidAccountType implements ValidationRule #[\Override] public function validate(string $attribute, mixed $value, Closure $fail): void { - if (!is_array($value)) { - $value = [$value]; - } // only check the type. if (array_key_exists('type', $value)) { $value = $value['type']; + if (!is_array($value)) { + $value = [$value]; + } + + $filtered = []; $keys = array_keys($this->types); /** @var mixed $entry */ diff --git a/app/Support/JsonApi/CollectsCustomParameters.php b/app/Support/JsonApi/CollectsCustomParameters.php index c45cffc1af..cab9d32fd4 100644 --- a/app/Support/JsonApi/CollectsCustomParameters.php +++ b/app/Support/JsonApi/CollectsCustomParameters.php @@ -37,6 +37,9 @@ trait CollectsCustomParameters if (array_key_exists('endPeriod', $params)) { $return['end'] = Carbon::parse($params['endPeriod']); } + if(array_key_exists('currentMoment', $params)) { + $return['today'] = Carbon::parse($params['currentMoment']); + } return $return; } diff --git a/app/Support/JsonApi/SortsQueryResults.php b/app/Support/JsonApi/SortsQueryResults.php new file mode 100644 index 0000000000..20a4e9ce5a --- /dev/null +++ b/app/Support/JsonApi/SortsQueryResults.php @@ -0,0 +1,133 @@ +all() as $field) { + $collection = $this->sortQueryCollection($class, $collection, $field); + } + return $collection; + } + + /** + * TODO improve this. + * + * @param string $class + * @param Collection $collection + * @param SortField $field + * + * @return Collection + */ + private function sortQueryCollection(string $class, Collection $collection, SortField $field): Collection + { + // here be custom sort things. + // sort by balance + if (Account::class === $class && 'balance' === $field->name()) { + $ascending = $field->isAscending(); + $collection = $collection->sort(function (Account $left, Account $right) use ($ascending): int { + $leftSum = $this->sumBalance($left->balance); + $rightSum = $this->sumBalance($right->balance); + return $ascending ? bccomp($leftSum, $rightSum) : bccomp($rightSum, $leftSum); + }); + } + if (Account::class === $class && 'balance_difference' === $field->name()) { + $ascending = $field->isAscending(); + $collection = $collection->sort(function (Account $left, Account $right) use ($ascending): int { + $leftSum = $this->sumBalanceDifference($left->balance); + $rightSum = $this->sumBalanceDifference($right->balance); + return $ascending ? bccomp($leftSum, $rightSum) : bccomp($rightSum, $leftSum); + }); + } + // sort by account number + if (Account::class === $class && 'account_number' === $field->name()) { + $ascending = $field->isAscending(); + $collection = $collection->sort(function (Account $left, Account $right) use ($ascending): int { + $leftNr = sprintf('%s%s', $left->iban, $left->account_number); + $rightNr = sprintf('%s%s', $right->iban, $right->account_number); + return $ascending ? strcmp($leftNr, $rightNr) : strcmp($rightNr, $leftNr); + }); + } + + // sort by last activity + if (Account::class === $class && 'last_activity' === $field->name()) { + $ascending = $field->isAscending(); + $collection = $collection->sort(function (Account $left, Account $right) use ($ascending): int { + $leftNr = (int)$left->last_activity?->format('U'); + $rightNr = (int)$right->last_activity?->format('U'); + if($ascending){ + return $leftNr <=> $rightNr; + } + return $rightNr <=> $leftNr; + //return (int) ($ascending ? $rightNr < $leftNr : $leftNr < $rightNr ); + }); + } + + // sort by balance difference. + + + return $collection; + } + + private function sumBalance(?array $balance): string + { + if (null === $balance) { + return '-10000000000'; // minus one billion + } + if (0 === count($balance)) { + return '-10000000000'; // minus one billion + } + $sum = '0'; + foreach ($balance as $entry) { + $sum = bcadd($sum, $entry['balance']); + } + return $sum; + } + + private function sumBalanceDifference(?array $balance): string + { + if (null === $balance) { + return '-10000000000'; // minus one billion + } + if (0 === count($balance)) { + return '-10000000000'; // minus one billion + } + $sum = '0'; + foreach ($balance as $entry) { + $sum = bcadd($sum, $entry['balance_difference']); + } + return $sum; + } + +} diff --git a/app/Support/JsonApi/ValidateSortParameters.php b/app/Support/JsonApi/ValidateSortParameters.php index 3f76a922d5..c173c3bc62 100644 --- a/app/Support/JsonApi/ValidateSortParameters.php +++ b/app/Support/JsonApi/ValidateSortParameters.php @@ -34,9 +34,7 @@ trait ValidateSortParameters if (null === $params) { return false; } - - $config = config(sprintf('api.full_data_set.%s', $class)) ?? []; - + $config = config('api.full_data_set')[$class] ?? []; foreach ($params->all() as $field) { if (in_array($field->name(), $config, true)) { Log::debug('TRUE'); diff --git a/config/api.php b/config/api.php index d78a033250..7b10eb267e 100644 --- a/config/api.php +++ b/config/api.php @@ -46,15 +46,16 @@ return [ 'accounts' => ['name', 'active', 'iban', 'order', 'account_number', 'balance', 'last_activity', 'balance_difference', 'current_debt'], ], ], - // valid query columns for sorting: + // valid query columns for sorting the query 'valid_query_sort' => [ - Account::class => ['id','name', 'active', 'iban', 'order'], + Account::class => ['id', 'name', 'active', 'iban', 'order'], ], - 'valid_api_sort' => [ - Account::class => [], + // valid query columns for sorting the query results + 'valid_api_sort' => [ + Account::class => ['account_number'], ], 'full_data_set' => [ - 'account' => ['last_activity', 'balance_difference', 'current_balance', 'current_debt'], + Account::class => ['last_activity', 'balance', 'balance_difference', 'current_debt', 'account_number'], ], 'valid_query_filters' => [ Account::class => ['id', 'name', 'iban', 'active'], diff --git a/resources/assets/v2/src/pages/accounts/index.js b/resources/assets/v2/src/pages/accounts/index.js index 57641d7909..eb8e70bd25 100644 --- a/resources/assets/v2/src/pages/accounts/index.js +++ b/resources/assets/v2/src/pages/accounts/index.js @@ -48,7 +48,12 @@ const params = new Proxy(new URLSearchParams(window.location.search), { get: (searchParams, prop) => searchParams.get(prop), }); sortingColumn = params.column ?? ''; -sortDirection = params.direction ?? ''; +sortDirection = 'asc'; +if(sortingColumn[0] === '-') { + sortingColumn = sortingColumn.substring(1); + sortDirection = 'desc'; +} + page = parseInt(params.page ?? 1); @@ -77,6 +82,7 @@ let index = function () { filters: { active: null, name: null, + type: type, }, pageOptions: { isLoading: true, @@ -345,10 +351,11 @@ let index = function () { //const sorting = [{column: this.pageOptions.sortingColumn, direction: this.pageOptions.sortDirection}]; // filter instructions - let filters = []; + 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]}); + filters[k] = this.filters[k]; + //filters.push({column: k, filter: this.filters[k]}); } } @@ -358,9 +365,9 @@ let index = function () { const today = new Date(); let params = { - sorting: sorting, - filters: filters, - // today: today, + sort: sorting, + filter: filters, + currentMoment: today, // type: type, page: {number: this.page}, startPeriod: start, @@ -368,8 +375,8 @@ let index = function () { }; if (!this.tableColumns.balance_difference.enabled) { - delete params.start; - delete params.end; + delete params.startPeriod; + delete params.enPeriod; } this.accounts = []; let groupedAccounts = {}; @@ -404,7 +411,6 @@ let index = function () { balance: current.attributes.balance, native_balance: current.attributes.native_balance, }; - // get group info: let groupId = current.attributes.object_group_id; if(!this.pageOptions.groupedAccounts) { diff --git a/resources/views/v2/accounts/index.blade.php b/resources/views/v2/accounts/index.blade.php index 93f3080ce6..8a043a0ef3 100644 --- a/resources/views/v2/accounts/index.blade.php +++ b/resources/views/v2/accounts/index.blade.php @@ -125,12 +125,12 @@ {{ __('list.interest') }}