New endpoint, fixed logo, better account overview.

This commit is contained in:
James Cole 2024-03-09 19:31:27 +01:00
parent f2c9e20aef
commit 9078781d61
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
17 changed files with 412 additions and 157 deletions

View File

@ -158,7 +158,8 @@ class Controller extends BaseController
// the transformer, at this point, needs to collect information that ALL items in the collection // 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. // require, like meta-data and stuff like that, and save it for later.
$transformer->collectMetaData($objects); $objects = $transformer->collectMetaData($objects);
$paginator->setCollection($objects);
$resource = new FractalCollection($objects, $transformer, $key); $resource = new FractalCollection($objects, $transformer, $key);
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); $resource->setPaginator(new IlluminatePaginatorAdapter($paginator));

View File

@ -47,7 +47,7 @@ class IndexController extends Controller
function ($request, $next) { function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class); $this->repository = app(AccountRepositoryInterface::class);
// new way of user group validation // new way of user group validation
$userGroup = $this->validateUserGroup($request); $userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) { if (null !== $userGroup) {
$this->repository->setUserGroup($userGroup); $this->repository->setUserGroup($userGroup);
} }
@ -62,20 +62,23 @@ class IndexController extends Controller
*/ */
public function index(IndexRequest $request): JsonResponse public function index(IndexRequest $request): JsonResponse
{ {
$this->repository->resetAccountOrder(); $this->repository->resetAccountOrder();
$types = $request->getAccountTypes(); $types = $request->getAccountTypes();
$accounts = $this->repository->getAccountsByType($types); $instructions = $request->getSortInstructions('accounts');
$pageSize = $this->parameters->get('limit'); $accounts = $this->repository->getAccountsByType($types, $instructions);
$count = $accounts->count(); $pageSize = $this->parameters->get('limit');
$accounts = $accounts->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); $count = $accounts->count();
$paginator = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page')); $accounts = $accounts->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
$transformer = new AccountTransformer(); $paginator = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page'));
$transformer = new AccountTransformer();
$this->parameters->set('sort', $instructions);
$transformer->setParameters($this->parameters); // give params to transformer $transformer->setParameters($this->parameters); // give params to transformer
return response() return response()
->json($this->jsonApiList('accounts', $paginator, $transformer)) ->json($this->jsonApiList('accounts', $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE) ->header('Content-Type', self::CONTENT_TYPE);
;
} }
public function infiniteList(InfiniteListRequest $request): JsonResponse public function infiniteList(InfiniteListRequest $request): JsonResponse
@ -83,7 +86,7 @@ class IndexController extends Controller
$this->repository->resetAccountOrder(); $this->repository->resetAccountOrder();
// get accounts of the specified type, and return. // get accounts of the specified type, and return.
$types = $request->getAccountTypes(); $types = $request->getAccountTypes();
// get from repository // get from repository
$accounts = $this->repository->getAccountsInOrder($types, $request->getSortInstructions('accounts'), $request->getStartRow(), $request->getEndRow()); $accounts = $this->repository->getAccountsInOrder($types, $request->getSortInstructions('accounts'), $request->getStartRow(), $request->getEndRow());
@ -95,7 +98,6 @@ class IndexController extends Controller
return response() return response()
->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE) ->header('Content-Type', self::CONTENT_TYPE);
;
} }
} }

View File

@ -27,6 +27,7 @@ use Carbon\Carbon;
use FireflyIII\Support\Http\Api\AccountFilter; use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;
use FireflyIII\Support\Request\GetSortInstructions;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
/** /**
@ -39,6 +40,7 @@ class IndexRequest extends FormRequest
use AccountFilter; use AccountFilter;
use ChecksLogin; use ChecksLogin;
use ConvertsDataTypes; use ConvertsDataTypes;
use GetSortInstructions;
/** /**
* Get all data from the request. * Get all data from the request.

View File

@ -29,6 +29,7 @@ use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\Support\Http\Api\TransactionFilter; use FireflyIII\Support\Http\Api\TransactionFilter;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;
use FireflyIII\Support\Request\GetSortInstructions;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
/** /**
@ -41,6 +42,7 @@ class InfiniteListRequest extends FormRequest
use ChecksLogin; use ChecksLogin;
use ConvertsDataTypes; use ConvertsDataTypes;
use TransactionFilter; use TransactionFilter;
use GetSortInstructions;
public function buildParams(): string public function buildParams(): string
{ {
@ -97,30 +99,6 @@ class InfiniteListRequest extends FormRequest
return 0 === $page || $page > 65536 ? 1 : $page; return 0 === $page || $page > 65536 ? 1 : $page;
} }
public function getSortInstructions(string $key): array
{
$allowed = config(sprintf('firefly.sorting.allowed.%s', $key));
$set = $this->get('sorting', []);
$result = [];
if (0 === count($set)) {
return [];
}
foreach ($set as $info) {
$column = $info['column'] ?? 'NOPE';
$direction = $info['direction'] ?? 'NOPE';
if ('asc' !== $direction && 'desc' !== $direction) {
// skip invalid direction
continue;
}
if (false === in_array($column, $allowed, true)) {
// skip invalid column
continue;
}
$result[$column] = $direction;
}
return $result;
}
public function getTransactionTypes(): array public function getTransactionTypes(): array
{ {

View File

@ -95,21 +95,21 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @method static Builder|Account withTrashed() * @method static Builder|Account withTrashed()
* @method static Builder|Account withoutTrashed() * @method static Builder|Account withoutTrashed()
* *
* @property Carbon $lastActivityDate * @property Carbon $lastActivityDate
* @property string $startBalance * @property string $startBalance
* @property string $endBalance * @property string $endBalance
* @property string $difference * @property string $difference
* @property string $interest * @property string $interest
* @property string $interestPeriod * @property string $interestPeriod
* @property string $accountTypeString * @property string $accountTypeString
* @property Location $location * @property Location $location
* @property string $liability_direction * @property string $liability_direction
* @property string $current_debt * @property string $current_debt
* @property int $user_group_id * @property int $user_group_id
* *
* @method static EloquentBuilder|Account whereUserGroupId($value) * @method static EloquentBuilder|Account whereUserGroupId($value)
* *
* @property null|UserGroup $userGroup * @property null|UserGroup $userGroup
* *
* @mixin Eloquent * @mixin Eloquent
*/ */
@ -121,18 +121,18 @@ class Account extends Model
use SoftDeletes; use SoftDeletes;
protected $casts protected $casts
= [ = [
'created_at' => 'datetime', 'created_at' => 'datetime',
'updated_at' => 'datetime', 'updated_at' => 'datetime',
'user_id' => 'integer', 'user_id' => 'integer',
'deleted_at' => 'datetime', 'deleted_at' => 'datetime',
'active' => 'boolean', 'active' => 'boolean',
'encrypted' => 'boolean', 'encrypted' => 'boolean',
]; ];
protected $fillable = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban']; protected $fillable = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban'];
protected $hidden = ['encrypted']; protected $hidden = ['encrypted'];
private bool $joinedAccountTypes = false; private bool $joinedAccountTypes = false;
/** /**
@ -146,10 +146,10 @@ class Account extends Model
$accountId = (int)$value; $accountId = (int)$value;
/** @var User $user */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
/** @var null|Account $account */ /** @var null|Account $account */
$account = $user->accounts()->with(['accountType'])->find($accountId); $account = $user->accounts()->with(['accountType'])->find($accountId);
if (null !== $account) { if (null !== $account) {
return $account; return $account;
} }
@ -180,9 +180,8 @@ class Account extends Model
{ {
/** @var null|AccountMeta $metaValue */ /** @var null|AccountMeta $metaValue */
$metaValue = $this->accountMeta() $metaValue = $this->accountMeta()
->where('name', 'account_number') ->where('name', 'account_number')
->first() ->first();
;
return null !== $metaValue ? $metaValue->data : ''; return null !== $metaValue ? $metaValue->data : '';
} }
@ -240,7 +239,7 @@ class Account extends Model
public function setVirtualBalanceAttribute(mixed $value): void public function setVirtualBalanceAttribute(mixed $value): void
{ {
$value = (string)$value; $value = (string)$value;
if ('' === $value) { if ('' === $value) {
$value = null; $value = null;
} }
@ -260,7 +259,7 @@ class Account extends Model
protected function accountId(): Attribute protected function accountId(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: static fn ($value) => (int)$value, get: static fn($value) => (int)$value,
); );
} }
@ -270,14 +269,22 @@ class Account extends Model
protected function accountTypeId(): Attribute protected function accountTypeId(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: static fn ($value) => (int)$value, get: static fn($value) => (int)$value,
); );
} }
//
protected function iban(): Attribute
{
return Attribute::make(
get: static fn($value) => null === $value ? null : trim(str_replace(' ', '', (string)$value)),
);
}
protected function order(): Attribute protected function order(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: static fn ($value) => (int)$value, get: static fn($value) => (int)$value,
); );
} }
@ -287,7 +294,7 @@ class Account extends Model
protected function virtualBalance(): Attribute protected function virtualBalance(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: static fn ($value) => (string)$value, get: static fn($value) => (string)$value,
); );
} }
} }

View File

@ -62,8 +62,7 @@ class AccountRepository implements AccountRepositoryInterface
$q1->where('account_meta.name', '=', 'account_number'); $q1->where('account_meta.name', '=', 'account_number');
$q1->where('account_meta.data', '=', $json); $q1->where('account_meta.data', '=', $json);
} }
) );
;
if (0 !== count($types)) { if (0 !== count($types)) {
$dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); $dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
@ -89,7 +88,7 @@ class AccountRepository implements AccountRepositoryInterface
public function findByName(string $name, array $types): ?Account public function findByName(string $name, array $types): ?Account
{ {
$query = $this->userGroup->accounts(); $query = $this->userGroup->accounts();
if (0 !== count($types)) { if (0 !== count($types)) {
$query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
@ -113,8 +112,8 @@ class AccountRepository implements AccountRepositoryInterface
public function getAccountCurrency(Account $account): ?TransactionCurrency public function getAccountCurrency(Account $account): ?TransactionCurrency
{ {
$type = $account->accountType->type; $type = $account->accountType->type;
$list = config('firefly.valid_currency_account_types'); $list = config('firefly.valid_currency_account_types');
// return null if not in this list. // return null if not in this list.
if (!in_array($type, $list, true)) { if (!in_array($type, $list, true)) {
@ -239,16 +238,19 @@ class AccountRepository implements AccountRepositoryInterface
public function getAccountsByType(array $types, ?array $sort = []): Collection public function getAccountsByType(array $types, ?array $sort = []): Collection
{ {
$res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types); $sortable = ['name', 'active']; // TODO yes this is a duplicate array.
$query = $this->userGroup->accounts(); $res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types);
$query = $this->userGroup->accounts();
if (0 !== count($types)) { if (0 !== count($types)) {
$query->accountTypeIn($types); $query->accountTypeIn($types);
} }
// add sort parameters. At this point they're filtered to allowed fields to sort by: // add sort parameters. At this point they're filtered to allowed fields to sort by:
if (0 !== count($sort)) { if (count($sort) > 0) {
foreach ($sort as $param) { foreach ($sort as $column => $direction) {
$query->orderBy($param[0], $param[1]); if (in_array($column, $sortable, true)) {
$query->orderBy(sprintf('accounts.%s', $column), $direction);
}
} }
} }
@ -267,12 +269,11 @@ class AccountRepository implements AccountRepositoryInterface
{ {
// search by group, not by user // search by group, not by user
$dbQuery = $this->userGroup->accounts() $dbQuery = $this->userGroup->accounts()
->where('active', true) ->where('active', true)
->orderBy('accounts.order', 'ASC') ->orderBy('accounts.order', 'ASC')
->orderBy('accounts.account_type_id', 'ASC') ->orderBy('accounts.account_type_id', 'ASC')
->orderBy('accounts.name', 'ASC') ->orderBy('accounts.name', 'ASC')
->with(['accountType']) ->with(['accountType']);
;
if ('' !== $query) { if ('' !== $query) {
// split query on spaces just in case: // split query on spaces just in case:
$parts = explode(' ', $query); $parts = explode(' ', $query);

View File

@ -0,0 +1,54 @@
<?php
/*
* GetSortInstructions.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\Support\Request;
trait GetSortInstructions
{
final public function getSortInstructions(string $key): array
{
$allowed = config(sprintf('firefly.sorting.allowed.%s', $key));
$set = $this->get('sorting', []);
$result = [];
if (0 === count($set)) {
return [];
}
foreach ($set as $info) {
$column = $info['column'] ?? 'NOPE';
$direction = $info['direction'] ?? 'NOPE';
if ('asc' !== $direction && 'desc' !== $direction) {
// skip invalid direction
continue;
}
if (false === in_array($column, $allowed, true)) {
// skip invalid column
continue;
}
$result[$column] = $direction;
}
return $result;
}
}

View File

@ -37,8 +37,11 @@ abstract class AbstractTransformer extends TransformerAbstract
/** /**
* This method is called exactly ONCE from FireflyIII\Api\V2\Controllers\Controller::jsonApiList * This method is called exactly ONCE from FireflyIII\Api\V2\Controllers\Controller::jsonApiList
*
* @param Collection $objects
* @return Collection
*/ */
abstract public function collectMetaData(Collection $objects): void; abstract public function collectMetaData(Collection $objects): Collection;
final public function getParameters(): ParameterBag final public function getParameters(): ParameterBag
{ {

View File

@ -32,6 +32,7 @@ use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/** /**
* Class AccountTransformer * Class AccountTransformer
@ -48,7 +49,7 @@ class AccountTransformer extends AbstractTransformer
/** /**
* @throws FireflyException * @throws FireflyException
*/ */
public function collectMetaData(Collection $objects): void public function collectMetaData(Collection $objects): Collection
{ {
$this->currencies = []; $this->currencies = [];
$this->accountMeta = []; $this->accountMeta = [];
@ -57,18 +58,17 @@ class AccountTransformer extends AbstractTransformer
$this->convertedBalances = app('steam')->balancesByAccountsConverted($objects, $this->getDate()); $this->convertedBalances = app('steam')->balancesByAccountsConverted($objects, $this->getDate());
/** @var CurrencyRepositoryInterface $repository */ /** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class); $repository = app(CurrencyRepositoryInterface::class);
$this->default = app('amount')->getDefaultCurrency(); $this->default = app('amount')->getDefaultCurrency();
// get currencies: // get currencies:
$accountIds = $objects->pluck('id')->toArray(); $accountIds = $objects->pluck('id')->toArray();
$meta = AccountMeta::whereIn('account_id', $accountIds) $meta = AccountMeta::whereIn('account_id', $accountIds)
->where('name', 'currency_id') ->whereIn('name', ['currency_id','account_role','account_number'])
->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data']) ->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data']);
; $currencyIds = $meta->where('name','currency_id')->pluck('data')->toArray();
$currencyIds = $meta->pluck('data')->toArray();
$currencies = $repository->getByIds($currencyIds); $currencies = $repository->getByIds($currencyIds);
foreach ($currencies as $currency) { foreach ($currencies as $currency) {
$id = $currency->id; $id = $currency->id;
$this->currencies[$id] = $currency; $this->currencies[$id] = $currency;
@ -79,15 +79,47 @@ class AccountTransformer extends AbstractTransformer
} }
// get account types: // get account types:
// select accounts.id, account_types.type from account_types left join accounts on accounts.account_type_id = account_types.id; // select accounts.id, account_types.type from account_types left join accounts on accounts.account_type_id = account_types.id;
$accountTypes = AccountType::leftJoin('accounts', 'accounts.account_type_id', '=', 'account_types.id') $accountTypes = AccountType::leftJoin('accounts', 'accounts.account_type_id', '=', 'account_types.id')
->whereIn('accounts.id', $accountIds) ->whereIn('accounts.id', $accountIds)
->get(['accounts.id', 'account_types.type']) ->get(['accounts.id', 'account_types.type']);
;
/** @var AccountType $row */ /** @var AccountType $row */
foreach ($accountTypes as $row) { foreach ($accountTypes as $row) {
$this->accountTypes[$row->id] = (string)config(sprintf('firefly.shortNamesByFullName.%s', $row->type)); $this->accountTypes[$row->id] = (string)config(sprintf('firefly.shortNamesByFullName.%s', $row->type));
} }
// TODO needs separate method.
$sort = $this->parameters->get('sort');
if (count($sort) > 0) {
foreach ($sort as $column => $direction) {
// account_number + iban
if ('iban' === $column) {
$meta = $this->accountMeta;
$objects = $objects->sort(function (Account $left, Account $right) use ($meta, $direction) {
$leftIban = trim(sprintf('%s%s', $left->iban, ($meta[$left->id]['account_number'] ?? '')));
$rightIban = trim(sprintf('%s%s', $right->iban, ($meta[$right->id]['account_number'] ?? '')));
if ('asc' === $direction) {
return strcasecmp($leftIban, $rightIban);
}
return strcasecmp($rightIban, $leftIban);
});
}
if('balance' === $column) {
$balances = $this->convertedBalances;
$objects = $objects->sort(function (Account $left, Account $right) use ($balances, $direction) {
$leftBalance = (float)($balances[$left->id]['native_balance'] ?? 0);
$rightBalance = (float)($balances[$right->id]['native_balance'] ?? 0);
if ('asc' === $direction) {
return $leftBalance <=> $rightBalance;
}
return $rightBalance <=> $leftBalance;
});
}
}
}
//$objects = $objects->sortByDesc('name');
return $objects;
} }
private function getDate(): Carbon private function getDate(): Carbon
@ -105,15 +137,15 @@ class AccountTransformer extends AbstractTransformer
*/ */
public function transform(Account $account): array public function transform(Account $account): array
{ {
$id = $account->id; $id = $account->id;
// various meta // various meta
$accountRole = $this->accountMeta[$id]['account_role'] ?? null; $accountRole = $this->accountMeta[$id]['account_role'] ?? null;
$accountType = $this->accountTypes[$id]; $accountType = $this->accountTypes[$id];
$order = $account->order; $order = $account->order;
// no currency? use default // no currency? use default
$currency = $this->default; $currency = $this->default;
if (array_key_exists($id, $this->accountMeta) && 0 !== (int)$this->accountMeta[$id]['currency_id']) { if (array_key_exists($id, $this->accountMeta) && 0 !== (int)$this->accountMeta[$id]['currency_id']) {
$currency = $this->currencies[(int)$this->accountMeta[$id]['currency_id']]; $currency = $this->currencies[(int)$this->accountMeta[$id]['currency_id']];
} }
@ -127,19 +159,20 @@ class AccountTransformer extends AbstractTransformer
} }
return [ return [
'id' => (string)$account->id, 'id' => (string)$account->id,
'created_at' => $account->created_at->toAtomString(), 'created_at' => $account->created_at->toAtomString(),
'updated_at' => $account->updated_at->toAtomString(), 'updated_at' => $account->updated_at->toAtomString(),
'active' => $account->active, 'active' => $account->active,
'order' => $order, 'order' => $order,
'name' => $account->name, 'name' => $account->name,
'iban' => '' === $account->iban ? null : $account->iban, 'iban' => '' === (string)$account->iban ? null : $account->iban,
'type' => strtolower($accountType), 'account_number' => $this->accountMeta[$id]['account_number'] ?? null,
'account_role' => $accountRole, 'type' => strtolower($accountType),
'currency_id' => (string)$currency->id, 'account_role' => $accountRole,
'currency_code' => $currency->code, 'currency_id' => (string)$currency->id,
'currency_symbol' => $currency->symbol, 'currency_code' => $currency->code,
'currency_decimal_places' => $currency->decimal_places, 'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'native_currency_id' => (string)$this->default->id, 'native_currency_id' => (string)$this->default->id,
'native_currency_code' => $this->default->code, 'native_currency_code' => $this->default->code,
@ -173,7 +206,7 @@ class AccountTransformer extends AbstractTransformer
'links' => [ 'links' => [
[ [
'rel' => 'self', 'rel' => 'self',
'uri' => '/accounts/'.$account->id, 'uri' => '/accounts/' . $account->id,
], ],
], ],
]; ];

View File

@ -432,7 +432,7 @@ return [
'transfers' => 'fa-exchange', 'transfers' => 'fa-exchange',
], ],
'bindables' => [ 'bindables' => [
// models // models
'account' => Account::class, 'account' => Account::class,
'attachment' => Attachment::class, 'attachment' => Attachment::class,
@ -490,7 +490,7 @@ return [
'userGroupBill' => UserGroupBill::class, 'userGroupBill' => UserGroupBill::class,
'userGroup' => UserGroup::class, 'userGroup' => UserGroup::class,
], ],
'rule-actions' => [ 'rule-actions' => [
'set_category' => SetCategory::class, 'set_category' => SetCategory::class,
'clear_category' => ClearCategory::class, 'clear_category' => ClearCategory::class,
'set_budget' => SetBudget::class, 'set_budget' => SetBudget::class,
@ -521,7 +521,7 @@ return [
'set_source_to_cash' => SetSourceToCashAccount::class, 'set_source_to_cash' => SetSourceToCashAccount::class,
'set_destination_to_cash' => SetDestinationToCashAccount::class, 'set_destination_to_cash' => SetDestinationToCashAccount::class,
], ],
'context-rule-actions' => [ 'context-rule-actions' => [
'set_category', 'set_category',
'set_budget', 'set_budget',
'add_tag', 'add_tag',
@ -540,13 +540,13 @@ return [
'convert_transfer', 'convert_transfer',
], ],
'test-triggers' => [ 'test-triggers' => [
'limit' => 10, 'limit' => 10,
'range' => 200, 'range' => 200,
], ],
// expected source types for each transaction type, in order of preference. // expected source types for each transaction type, in order of preference.
'expected_source_types' => [ 'expected_source_types' => [
'source' => [ 'source' => [
TransactionTypeModel::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionTypeModel::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeEnum::DEPOSIT->value => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::REVENUE, AccountType::CASH], TransactionTypeEnum::DEPOSIT->value => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::REVENUE, AccountType::CASH],
@ -591,7 +591,7 @@ return [
TransactionTypeModel::LIABILITY_CREDIT => [AccountType::LIABILITY_CREDIT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionTypeModel::LIABILITY_CREDIT => [AccountType::LIABILITY_CREDIT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
], ],
], ],
'allowed_opposing_types' => [ 'allowed_opposing_types' => [
'source' => [ 'source' => [
AccountType::ASSET => [ AccountType::ASSET => [
AccountType::ASSET, AccountType::ASSET,
@ -681,7 +681,7 @@ return [
], ],
], ],
// depending on the account type, return the allowed transaction types: // depending on the account type, return the allowed transaction types:
'allowed_transaction_types' => [ 'allowed_transaction_types' => [
'source' => [ 'source' => [
AccountType::ASSET => [ AccountType::ASSET => [
TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::WITHDRAWAL,
@ -750,7 +750,7 @@ return [
], ],
// having the source + dest will tell you the transaction type. // having the source + dest will tell you the transaction type.
'account_to_transaction' => [ 'account_to_transaction' => [
AccountType::ASSET => [ AccountType::ASSET => [
AccountType::ASSET => TransactionTypeModel::TRANSFER, AccountType::ASSET => TransactionTypeModel::TRANSFER,
AccountType::CASH => TransactionTypeModel::WITHDRAWAL, AccountType::CASH => TransactionTypeModel::WITHDRAWAL,
@ -815,7 +815,7 @@ return [
], ],
// allowed source -> destination accounts. // allowed source -> destination accounts.
'source_dests' => [ 'source_dests' => [
TransactionTypeModel::WITHDRAWAL => [ TransactionTypeModel::WITHDRAWAL => [
AccountType::ASSET => [AccountType::EXPENSE, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::CASH], AccountType::ASSET => [AccountType::EXPENSE, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::CASH],
AccountType::LOAN => [AccountType::EXPENSE, AccountType::CASH], AccountType::LOAN => [AccountType::EXPENSE, AccountType::CASH],
@ -854,7 +854,7 @@ return [
], ],
], ],
// if you add fields to this array, don't forget to update the export routine (ExportDataGenerator). // if you add fields to this array, don't forget to update the export routine (ExportDataGenerator).
'journal_meta_fields' => [ 'journal_meta_fields' => [
// sepa // sepa
'sepa_cc', 'sepa_cc',
'sepa_ct_op', 'sepa_ct_op',
@ -888,36 +888,36 @@ return [
'recurrence_count', 'recurrence_count',
'recurrence_date', 'recurrence_date',
], ],
'webhooks' => [ 'webhooks' => [
'max_attempts' => env('WEBHOOK_MAX_ATTEMPTS', 3), 'max_attempts' => env('WEBHOOK_MAX_ATTEMPTS', 3),
], ],
'can_have_virtual_amounts' => [AccountType::ASSET], 'can_have_virtual_amounts' => [AccountType::ASSET],
'can_have_opening_balance' => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], 'can_have_opening_balance' => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
'dynamic_creation_allowed' => [ 'dynamic_creation_allowed' => [
AccountType::EXPENSE, AccountType::EXPENSE,
AccountType::REVENUE, AccountType::REVENUE,
AccountType::INITIAL_BALANCE, AccountType::INITIAL_BALANCE,
AccountType::RECONCILIATION, AccountType::RECONCILIATION,
AccountType::LIABILITY_CREDIT, AccountType::LIABILITY_CREDIT,
], ],
'valid_asset_fields' => ['account_role', 'account_number', 'currency_id', 'BIC', 'include_net_worth'], '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_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_account_fields' => ['account_number', 'currency_id', 'BIC', 'interest', 'interest_period', 'include_net_worth', 'liability_direction'],
// dynamic date ranges are as follows: // 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 // only used in v1
'allowed_sort_parameters' => ['order', 'name', 'iban'], 'allowed_sort_parameters' => ['order', 'name', 'iban'],
// preselected account lists possibilities: // preselected account lists possibilities:
'preselected_accounts' => ['all', 'assets', 'liabilities'], 'preselected_accounts' => ['all', 'assets', 'liabilities'],
// allowed sort columns for API's // allowed sort columns for API's
'sorting' => [ 'sorting' => [
'allowed' => [ 'allowed' => [
'transactions' => ['description', 'amount'], 'transactions' => ['description', 'amount'],
'accounts' => ['name'], 'accounts' => ['name', 'active', 'iban', 'balance'],
], ],
], ],
]; ];

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -36,12 +36,16 @@ import {getVariable} from "../store/get-variable.js";
import {getViewRange} from "../support/get-viewrange.js"; import {getViewRange} from "../support/get-viewrange.js";
import {loadTranslations} from "../support/load-translations.js"; import {loadTranslations} from "../support/load-translations.js";
import adminlte from 'admin-lte';
store.addPlugin(observePlugin); store.addPlugin(observePlugin);
window.bootstrapped = false; window.bootstrapped = false;
window.store = store; window.store = store;
// always grab the preference "marker" from Firefly III. // always grab the preference "marker" from Firefly III.
getFreshVariable('lastActivity').then((serverValue) => { getFreshVariable('lastActivity').then((serverValue) => {
const localValue = store.get('lastActivity'); const localValue = store.get('lastActivity');

View File

@ -51,9 +51,17 @@ let index = function () {
enabled: true enabled: true
}, },
}, },
sortingColumn: '',
sortDirection: '',
accounts: [], accounts: [],
sort(column) {
this.sortingColumn = column;
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
this.loadAccounts();
return false;
},
formatMoney(amount, currencyCode) { formatMoney(amount, currencyCode) {
return formatMoney(amount, currencyCode); return formatMoney(amount, currencyCode);
}, },
@ -69,14 +77,29 @@ let index = function () {
}, },
loadAccounts() { loadAccounts() {
this.notifications.wait.show = true;
this.notifications.wait.text = i18next.t('firefly.wait_loading_data')
this.accounts = [];
// sort instructions
// &sorting[0][column]=description&sorting[0][direction]=asc
const sorting = [{column: this.sortingColumn, direction: this.sortDirection}];
// one page only. // one page only.
(new Get()).index({type: type, page: this.page}).then(response => { (new Get()).index({sorting: sorting, type: type, page: this.page}).then(response => {
for (let i = 0; i < response.data.data.length; i++) { for (let i = 0; i < response.data.data.length; i++) {
if (response.data.data.hasOwnProperty(i)) { if (response.data.data.hasOwnProperty(i)) {
let current = response.data.data[i]; let current = response.data.data[i];
let account = { let account = {
id: parseInt(current.id), id: parseInt(current.id),
active: current.attributes.active,
name: current.attributes.name, name: current.attributes.name,
type: current.attributes.type,
// role: current.attributes.account_role,
iban: null === current.attributes.iban ? '' : current.attributes.iban.match(/.{1,4}/g).join(' '),
account_number: null === current.attributes.account_number ? '' : current.attributes.account_number,
current_balance: current.attributes.current_balance,
currency_code: current.attributes.currency_code,
native_current_balance: current.attributes.native_current_balance,
native_currency_code: current.attributes.native_currency_code,
}; };
this.accounts.push(account); this.accounts.push(account);
} }

View File

@ -0,0 +1,105 @@
/*!
* adminlte-filteres.scss
* 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/.
*/
// less adminlte code because the CSS is massive otherwise.
// copied from:
/*!
* AdminLTE v4.0.0-alpha2
* Author: Colorlib
* Website: AdminLTE.io <https://adminlte.io>
* License: Open source - MIT <https://opensource.org/licenses/MIT>
*/
// Bootstrap Configuration
// ---------------------------------------------------
@import "bootstrap/scss/functions";
// AdminLTE Configuration
// ---------------------------------------------------
@import "admin-lte/src/scss/bootstrap-variables"; // little modified are here
// Bootstrap Configuration
// ---------------------------------------------------
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";
// Bootstrap Layout & components
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/type";
@import "bootstrap/scss/images";
@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/tables";
@import "bootstrap/scss/forms";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/transitions";
@import "bootstrap/scss/dropdown";
@import "bootstrap/scss/button-group";
@import "bootstrap/scss/nav";
@import "bootstrap/scss/navbar";
@import "bootstrap/scss/card";
//@import "bootstrap/scss/accordion";
@import "bootstrap/scss/breadcrumb";
@import "bootstrap/scss/pagination";
@import "bootstrap/scss/badge";
@import "bootstrap/scss/alert";
@import "bootstrap/scss/progress";
@import "bootstrap/scss/list-group";
@import "bootstrap/scss/close";
//@import "bootstrap/scss/toasts";
@import "bootstrap/scss/modal";
@import "bootstrap/scss/tooltip";
@import "bootstrap/scss/popover";
//@import "bootstrap/scss/carousel";
@import "bootstrap/scss/spinners";
@import "bootstrap/scss/offcanvas";
@import "bootstrap/scss/placeholders";
// Bootstrap Helpers
@import "bootstrap/scss/helpers";
// Bootstrap Utilities
@import "bootstrap/scss/utilities/api";
// AdminLTE Configuration
// ---------------------------------------------------
@import "admin-lte/src/scss/variables";
@import "admin-lte/src/scss/variables-dark";
@import "admin-lte/src/scss/mixins";
// AdiminLTE Parts
// ---------------------------------------------------
@import "admin-lte/src/scss/parts/core";
@import "admin-lte/src/scss/parts/components";
@import "admin-lte/src/scss/parts/extra-components";
// @import "admin-lte/src/scss/parts/pages";
@import "admin-lte/src/scss/pages/login_and_register";
@import "admin-lte/src/scss/parts/miscellaneous";

View File

@ -38,7 +38,7 @@ $success: #64B624 !default;
// @import "bootstrap/scss/bootstrap"; // @import "bootstrap/scss/bootstrap";
// admin LTE // admin LTE
@import "admin-lte/src/scss/adminlte"; @import "adminlte-filtered";
// @import "~bootstrap-sass/assets/stylesheets/bootstrap"; // @import "~bootstrap-sass/assets/stylesheets/bootstrap";

View File

@ -57,11 +57,27 @@
<thead> <thead>
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td>Active?</td> <td>
<td>Name</td> <a href="#" x-on:click.prevent="sort('active')">Active?</a>
<em x-show="sortingColumn === 'active' && sortDirection === 'asc'" class="fa-solid fa-arrow-down-wide-short"></em>
<em x-show="sortingColumn === 'active' && sortDirection === 'desc'" class="fa-solid fa-arrow-up-wide-short"></em>
</td>
<td>
<a href="#" x-on:click.prevent="sort('name')">Name</a>
<em x-show="sortingColumn === 'name' && sortDirection === 'asc'" class="fa-solid fa-arrow-down-z-a"></em>
<em x-show="sortingColumn === 'name' && sortDirection === 'desc'" class="fa-solid fa-arrow-up-z-a"></em>
</td>
<td>Type</td> <td>Type</td>
<td>Account number</td> <td>
<td>Current balance</td> <a href="#" x-on:click.prevent="sort('iban')">Account number</a>
<em x-show="sortingColumn === 'iban' && sortDirection === 'asc'" class="fa-solid fa-arrow-down-z-a"></em>
<em x-show="sortingColumn === 'iban' && sortDirection === 'desc'" class="fa-solid fa-arrow-up-z-a"></em>
</td>
<td>
<a href="#" x-on:click.prevent="sort('balance')">Current balance</a>
<em x-show="sortingColumn === 'balance' && sortDirection === 'asc'" class="fa-solid fa-arrow-down-wide-short"></em>
<em x-show="sortingColumn === 'balance' && sortDirection === 'desc'" class="fa-solid fa-arrow-up-wide-short"></em>
</td>
<td>Last activity</td> <td>Last activity</td>
<td>Balance difference</td> <td>Balance difference</td>
<td>&nbsp;</td> <td>&nbsp;</td>
@ -70,20 +86,46 @@
<tbody> <tbody>
<template x-for="(account, index) in accounts" :key="index"> <template x-for="(account, index) in accounts" :key="index">
<tr> <tr>
<td>&nbsp;</td> <td>TODO</td>
<td> <td>
&nbsp; <template x-if="account.active">
<em class="text-success fa-solid fa-check"></em>
&nbsp;</template>
<template x-if="!account.active">
<em class="text-danger fa-solid fa-xmark"></em>
&nbsp;</template>
</td> </td>
<td> <td>
<a :href="'./accounts/show/' + account.id"> <a :href="'./accounts/show/' + account.id">
<span x-text="account.name"></span> <span x-text="account.name"></span>
</a> </a>
</td> </td>
<td></td> <td>
<td></td> <span x-text="account.type"></span>
<td></td> <span x-text="account.role"></span>
<td></td> </td>
<td></td> <td>
<!-- IBAN and no account nr -->
<template x-if="'' === account.account_number && '' !== account.iban">
<span x-text="account.iban + 'A'"></span>
</template>
<!-- no IBAN and account nr -->
<template x-if="'' !== account.account_number && '' === account.iban">
<span x-text="account.account_number"></span>
</template>
<!-- both -->
<template x-if="'' !== account.account_number && '' !== account.iban">
<span>
<span x-text="account.iban"></span>
(<span x-text="account.account_number"></span>)
</span>
</template>
</td>
<td>
<span x-text="formatMoney(account.current_balance, account.currency_code)"></span>
</td>
<td>TODO</td>
<td>TODO</td>
<td>&nbsp;</td> <td>&nbsp;</td>
</tr> </tr>
</template> </template>

View File

@ -4,7 +4,7 @@
<!--begin::Brand Link--> <!--begin::Brand Link-->
<a href="{{route('index') }}" class="brand-link"> <a href="{{route('index') }}" class="brand-link">
<!--begin::Brand Image--> <!--begin::Brand Image-->
<img src="v2/i/logo.png" alt="Firefly III Logo" <img src="images/logo.png" alt="Firefly III Logo"
class="brand-image opacity-75 shadow"> class="brand-image opacity-75 shadow">
<!--end::Brand Image--> <!--end::Brand Image-->
<!--begin::Brand Text--> <!--begin::Brand Text-->