diff --git a/app/Api/V2/Controllers/Controller.php b/app/Api/V2/Controllers/Controller.php index e10dcdc3e8..e22326cbef 100644 --- a/app/Api/V2/Controllers/Controller.php +++ b/app/Api/V2/Controllers/Controller.php @@ -122,6 +122,9 @@ class Controller extends BaseController $obj = null; } } + if(null !== $date && 'end' === $field) { + $obj->endOfDay(); + } $bag->set($field, $obj); } diff --git a/app/Api/V2/Controllers/Model/Account/IndexController.php b/app/Api/V2/Controllers/Model/Account/IndexController.php index 0b591a0452..09ab788310 100644 --- a/app/Api/V2/Controllers/Model/Account/IndexController.php +++ b/app/Api/V2/Controllers/Model/Account/IndexController.php @@ -58,7 +58,7 @@ class IndexController extends Controller } /** - * TODO see autocomplete/accountcontroller for list. + * TODO see autocomplete/account controller for list. */ public function index(IndexRequest $request): JsonResponse { @@ -77,28 +77,6 @@ class IndexController extends Controller return response() ->json($this->jsonApiList('accounts', $paginator, $transformer)) - ->header('Content-Type', self::CONTENT_TYPE) - ; - } - - public function infiniteList(InfiniteListRequest $request): JsonResponse - { - $this->repository->resetAccountOrder(); - - // get accounts of the specified type, and return. - $types = $request->getAccountTypes(); - - // get from repository - $accounts = $this->repository->getAccountsInOrder($types, $request->getSortInstructions('accounts'), $request->getStartRow(), $request->getEndRow()); - $total = $this->repository->countAccounts($types); - $count = $request->getEndRow() - $request->getStartRow(); - $paginator = new LengthAwarePaginator($accounts, $total, $count, $this->parameters->get('page')); - $transformer = new AccountTransformer(); - $transformer->setParameters($this->parameters); // give params to transformer - - return response() - ->json($this->jsonApiList(self::RESOURCE_KEY, $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 320cb0c6db..6597a5f3ba 100644 --- a/app/Api/V2/Request/Model/Account/IndexRequest.php +++ b/app/Api/V2/Request/Model/Account/IndexRequest.php @@ -44,14 +44,11 @@ class IndexRequest extends FormRequest public function getAccountTypes(): array { - $type = (string)$this->get('type', 'default'); + $type = (string) $this->get('type', 'default'); return $this->mapAccountTypes($type); } - /** - * Get all data from the request. - */ public function getDate(): Carbon { return $this->getCarbonDate('date'); @@ -63,7 +60,9 @@ class IndexRequest extends FormRequest public function rules(): array { return [ - 'date' => 'date|after:1900-01-01|before:2099-12-31', + 'date' => 'date|after:1900-01-01|before:2099-12-31', + 'start' => 'date|after:1900-01-01|before:2099-12-31|before:end|required_with:end', + 'end' => 'date|after:1900-01-01|before:2099-12-31|after:start|required_with:start', ]; } } diff --git a/app/Http/Middleware/SecureHeaders.php b/app/Http/Middleware/SecureHeaders.php index f386da0ef0..88880b0163 100644 --- a/app/Http/Middleware/SecureHeaders.php +++ b/app/Http/Middleware/SecureHeaders.php @@ -41,13 +41,16 @@ class SecureHeaders public function handle(Request $request, \Closure $next) { // generate and share nonce. - $nonce = base64_encode(random_bytes(16)); + $nonce = base64_encode(random_bytes(16)); Vite::useCspNonce($nonce); + if(class_exists('Barryvdh\Debugbar\Facades\Debugbar')) { + \Barryvdh\Debugbar\Facades\Debugbar::getJavascriptRenderer()->setCspNonce($nonce); + } app('view')->share('JS_NONCE', $nonce); - $response = $next($request); - $trackingScriptSrc = $this->getTrackingScriptSource(); - $csp = [ + $response = $next($request); + $trackingScriptSrc = $this->getTrackingScriptSource(); + $csp = [ "default-src 'none'", "object-src 'none'", sprintf("script-src 'unsafe-eval' 'strict-dynamic' 'nonce-%1s'", $nonce), @@ -55,14 +58,30 @@ class SecureHeaders "base-uri 'self'", "font-src 'self' data:", sprintf("connect-src 'self' %s", $trackingScriptSrc), - sprintf("img-src 'self' 'nonce-%1s'", $nonce), + sprintf("img-src 'self' data: 'nonce-%1s' ", $nonce), "manifest-src 'self'", ]; - $route = $request->route(); - $customUrl = ''; - $authGuard = (string)config('firefly.authentication_guard'); - $logoutUrl = (string)config('firefly.custom_logout_url'); + // overrule in development mode + if (true === env('IS_LOCAL_DEV')) { + $csp = [ + "default-src 'none'", + "object-src 'none'", + sprintf("script-src 'unsafe-eval' 'strict-dynamic' 'nonce-%1s' https://firefly.sd.internal/_debugbar/assets", $nonce), + "style-src 'unsafe-inline' 'self' https://10.0.0.15:5173/", + "base-uri 'self'", + "font-src 'self' data: https://10.0.0.15:5173/", + sprintf("connect-src 'self' %s https://10.0.0.15:5173/ wss://10.0.0.15:5173/", $trackingScriptSrc), + sprintf("img-src 'self' data: 'nonce-%1s'", $nonce), + "manifest-src 'self'", + ]; + } + + + $route = $request->route(); + $customUrl = ''; + $authGuard = (string) config('firefly.authentication_guard'); + $logoutUrl = (string) config('firefly.custom_logout_url'); if ('remote_user_guard' === $authGuard && '' !== $logoutUrl) { $customUrl = $logoutUrl; } @@ -71,7 +90,7 @@ class SecureHeaders $csp[] = sprintf("form-action 'self' %s", $customUrl); } - $featurePolicies = [ + $featurePolicies = [ "geolocation 'none'", "midi 'none'", // "notifications 'none'", @@ -110,8 +129,8 @@ class SecureHeaders */ private function getTrackingScriptSource(): string { - if ('' !== (string)config('firefly.tracker_site_id') && '' !== (string)config('firefly.tracker_url')) { - return (string)config('firefly.tracker_url'); + if ('' !== (string) config('firefly.tracker_site_id') && '' !== (string) config('firefly.tracker_url')) { + return (string) config('firefly.tracker_url'); } return ''; diff --git a/app/Repositories/UserGroup/UserGroupRepository.php b/app/Repositories/UserGroup/UserGroupRepository.php index 4148997d1b..996ace3154 100644 --- a/app/Repositories/UserGroup/UserGroupRepository.php +++ b/app/Repositories/UserGroup/UserGroupRepository.php @@ -295,4 +295,15 @@ class UserGroupRepository implements UserGroupRepositoryInterface $this->user->user_group_id = $userGroup->id; $this->user->save(); } + + #[\Override] public function getMembershipsFromGroupId(int $groupId): Collection + { + return $this->user->groupMemberships()->where('user_group_id', $groupId)->get(); + } + + + #[\Override] public function getById(int $id): ?UserGroup + { + return UserGroup::find($id); + } } diff --git a/app/Repositories/UserGroup/UserGroupRepositoryInterface.php b/app/Repositories/UserGroup/UserGroupRepositoryInterface.php index 5f025e186a..5a22f89d63 100644 --- a/app/Repositories/UserGroup/UserGroupRepositoryInterface.php +++ b/app/Repositories/UserGroup/UserGroupRepositoryInterface.php @@ -36,8 +36,12 @@ interface UserGroupRepositoryInterface { public function destroy(UserGroup $userGroup): void; + public function getMembershipsFromGroupId(int $groupId): Collection; + public function get(): Collection; + public function getById(int $id): ?UserGroup; + public function getAll(): Collection; public function setUser(null|Authenticatable|User $user): void; diff --git a/app/Repositories/UserGroups/Account/AccountRepository.php b/app/Repositories/UserGroups/Account/AccountRepository.php index 77f935fe67..3f6b74860c 100644 --- a/app/Repositories/UserGroups/Account/AccountRepository.php +++ b/app/Repositories/UserGroups/Account/AccountRepository.php @@ -27,11 +27,13 @@ namespace FireflyIII\Repositories\UserGroups\Account; use FireflyIII\Models\Account; use FireflyIII\Models\AccountMeta; use FireflyIII\Models\AccountType; +use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Services\Internal\Update\AccountUpdateService; use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; /** * Class AccountRepository @@ -63,8 +65,7 @@ class AccountRepository implements AccountRepositoryInterface $q1->where('account_meta.name', '=', 'account_number'); $q1->where('account_meta.data', '=', $json); } - ) - ; + ); if (0 !== count($types)) { $dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); @@ -90,7 +91,7 @@ class AccountRepository implements AccountRepositoryInterface public function findByName(string $name, array $types): ?Account { - $query = $this->userGroup->accounts(); + $query = $this->userGroup->accounts(); if (0 !== count($types)) { $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); @@ -114,14 +115,14 @@ class AccountRepository implements AccountRepositoryInterface public function getAccountCurrency(Account $account): ?TransactionCurrency { - $type = $account->accountType->type; - $list = config('firefly.valid_currency_account_types'); + $type = $account->accountType->type; + $list = config('firefly.valid_currency_account_types'); // return null if not in this list. if (!in_array($type, $list, true)) { return null; } - $currencyId = (int)$this->getMetaValue($account, 'currency_id'); + $currencyId = (int) $this->getMetaValue($account, 'currency_id'); if ($currencyId > 0) { return TransactionCurrency::find($currencyId); } @@ -143,7 +144,7 @@ class AccountRepository implements AccountRepositoryInterface return null; } if (1 === $result->count()) { - return (string)$result->first()->data; + return (string) $result->first()->data; } return null; @@ -228,7 +229,7 @@ class AccountRepository implements AccountRepositoryInterface continue; } - if ($index !== (int)$account->order) { + if ($index !== (int) $account->order) { app('log')->debug(sprintf('Account #%d ("%s"): order should %d be but is %d.', $account->id, $account->name, $index, $account->order)); $account->order = $index; $account->save(); @@ -240,9 +241,9 @@ class AccountRepository implements AccountRepositoryInterface public function getAccountsByType(array $types, ?array $sort = []): Collection { - $sortable = ['name', 'active']; // TODO yes this is a duplicate array. - $res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types); - $query = $this->userGroup->accounts(); + $sortable = ['name', 'active']; // TODO yes this is a duplicate array. + $res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types); + $query = $this->userGroup->accounts(); if (0 !== count($types)) { $query->accountTypeIn($types); } @@ -275,12 +276,11 @@ class AccountRepository implements AccountRepositoryInterface { // search by group, not by user $dbQuery = $this->userGroup->accounts() - ->where('active', true) - ->orderBy('accounts.order', 'ASC') - ->orderBy('accounts.account_type_id', 'ASC') - ->orderBy('accounts.name', 'ASC') - ->with(['accountType']) - ; + ->where('active', true) + ->orderBy('accounts.order', 'ASC') + ->orderBy('accounts.account_type_id', 'ASC') + ->orderBy('accounts.name', 'ASC') + ->with(['accountType']); if ('' !== $query) { // split query on spaces just in case: $parts = explode(' ', $query); @@ -305,4 +305,30 @@ class AccountRepository implements AccountRepositoryInterface return $service->update($account, $data); } + + #[\Override] public function getMetaValues(Collection $accounts, array $fields): Collection + { + $query = AccountMeta::whereIn('account_id', $accounts->pluck('id')->toArray()); + if (count($fields) > 0) { + $query->whereIn('name', $fields); + } + return $query->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data']); + } + + #[\Override] public function getAccountTypes(Collection $accounts): Collection + { + return AccountType::leftJoin('accounts', 'accounts.account_type_id', '=', 'account_types.id') + ->whereIn('accounts.id', $accounts->pluck('id')->toArray()) + ->get(['accounts.id', 'account_types.type']); + + } + + #[\Override] public function getLastActivity(Collection $accounts): array + { + return Transaction::whereIn('account_id', $accounts->pluck('id')->toArray()) + ->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id') + ->groupBy('transactions.account_id') + ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) as date_max')])->toArray() // @phpstan-ignore-line + ; + } } diff --git a/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php b/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php index a736a54444..1700e09f13 100644 --- a/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php +++ b/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php @@ -37,6 +37,12 @@ interface AccountRepositoryInterface { public function countAccounts(array $types): int; + public function getAccountTypes(Collection $accounts): Collection; + + public function getLastActivity(Collection $accounts): array; + + public function getMetaValues(Collection $accounts, array $fields): Collection; + public function find(int $accountId): ?Account; public function findByAccountNumber(string $number, array $types): ?Account; diff --git a/app/Support/Http/Api/ValidatesUserGroupTrait.php b/app/Support/Http/Api/ValidatesUserGroupTrait.php index af031ab778..8491dd74dd 100644 --- a/app/Support/Http/Api/ValidatesUserGroupTrait.php +++ b/app/Support/Http/Api/ValidatesUserGroupTrait.php @@ -24,12 +24,13 @@ declare(strict_types=1); namespace FireflyIII\Support\Http\Api; use FireflyIII\Enums\UserRoleEnum; -use FireflyIII\Models\GroupMembership; use FireflyIII\Models\UserGroup; +use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface; use FireflyIII\User; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; /** * Trait ValidatesUserGroupTrait @@ -42,63 +43,63 @@ trait ValidatesUserGroupTrait */ protected function validateUserGroup(Request $request): UserGroup { - app('log')->debug(sprintf('validateUserGroup: %s', static::class)); + Log::debug(sprintf('validateUserGroup: %s', static::class)); if (!auth()->check()) { - app('log')->debug('validateUserGroup: user is not logged in, return NULL.'); + Log::debug('validateUserGroup: user is not logged in, return NULL.'); throw new AuthenticationException(); } /** @var User $user */ - $user = auth()->user(); - $groupId = 0; + $user = auth()->user(); + $groupId = 0; if (!$request->has('user_group_id')) { $groupId = $user->user_group_id; - app('log')->debug(sprintf('validateUserGroup: no user group submitted, use default group #%d.', $groupId)); + Log::debug(sprintf('validateUserGroup: no user group submitted, use default group #%d.', $groupId)); } if ($request->has('user_group_id')) { - $groupId = (int)$request->get('user_group_id'); - app('log')->debug(sprintf('validateUserGroup: user group submitted, search for memberships in group #%d.', $groupId)); + $groupId = (int) $request->get('user_group_id'); + Log::debug(sprintf('validateUserGroup: user group submitted, search for memberships in group #%d.', $groupId)); } + /** @var UserGroupRepositoryInterface $repository */ + $repository = app(UserGroupRepositoryInterface::class); + $repository->setUser($user); + $memberships = $repository->getMembershipsFromGroupId($groupId); - /** @var null|GroupMembership $membership */ - $membership = $user->groupMemberships()->where('user_group_id', $groupId)->first(); + if (0 === $memberships->count()) { + Log::debug(sprintf('validateUserGroup: user has no access to group #%d.', $groupId)); - if (null === $membership) { - app('log')->debug(sprintf('validateUserGroup: user has no access to group #%d.', $groupId)); - - throw new AuthorizationException((string)trans('validation.no_access_group')); + throw new AuthorizationException((string) trans('validation.no_access_group')); } // need to get the group from the membership: - /** @var null|UserGroup $group */ - $group = $membership->userGroup; + $group = $repository->getById($groupId); if (null === $group) { - app('log')->debug(sprintf('validateUserGroup: group #%d does not exist.', $groupId)); + Log::debug(sprintf('validateUserGroup: group #%d does not exist.', $groupId)); - throw new AuthorizationException((string)trans('validation.belongs_user_or_user_group')); + throw new AuthorizationException((string) trans('validation.belongs_user_or_user_group')); } - app('log')->debug(sprintf('validateUserGroup: validate access of user to group #%d ("%s").', $groupId, $group->title)); - $roles = property_exists($this, 'acceptedRoles') ? $this->acceptedRoles : []; + Log::debug(sprintf('validateUserGroup: validate access of user to group #%d ("%s").', $groupId, $group->title)); + $roles = property_exists($this, 'acceptedRoles') ? $this->acceptedRoles : []; if (0 === count($roles)) { - app('log')->debug('validateUserGroup: no roles defined, so no access.'); + Log::debug('validateUserGroup: no roles defined, so no access.'); - throw new AuthorizationException((string)trans('validation.no_accepted_roles_defined')); + throw new AuthorizationException((string) trans('validation.no_accepted_roles_defined')); } - app('log')->debug(sprintf('validateUserGroup: have %d roles to check.', count($roles)), $roles); + Log::debug(sprintf('validateUserGroup: have %d roles to check.', count($roles)), $roles); /** @var UserRoleEnum $role */ foreach ($roles as $role) { if ($user->hasRoleInGroupOrOwner($group, $role)) { - app('log')->debug(sprintf('validateUserGroup: User has role "%s" in group #%d, return the group.', $role->value, $groupId)); + Log::debug(sprintf('validateUserGroup: User has role "%s" in group #%d, return the group.', $role->value, $groupId)); return $group; } - app('log')->debug(sprintf('validateUserGroup: User does NOT have role "%s" in group #%d, continue searching.', $role->value, $groupId)); + Log::debug(sprintf('validateUserGroup: User does NOT have role "%s" in group #%d, continue searching.', $role->value, $groupId)); } - app('log')->debug('validateUserGroup: User does NOT have enough rights to access endpoint.'); + Log::debug('validateUserGroup: User does NOT have enough rights to access endpoint.'); - throw new AuthorizationException((string)trans('validation.belongs_user_or_user_group')); + throw new AuthorizationException((string) trans('validation.belongs_user_or_user_group')); } } diff --git a/app/Transformers/V2/AccountTransformer.php b/app/Transformers/V2/AccountTransformer.php index b0a8088800..fc719e1917 100644 --- a/app/Transformers/V2/AccountTransformer.php +++ b/app/Transformers/V2/AccountTransformer.php @@ -27,13 +27,12 @@ namespace FireflyIII\Transformers\V2; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; -use FireflyIII\Models\AccountMeta; use FireflyIII\Models\AccountType; -use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; /** * Class AccountTransformer @@ -42,118 +41,47 @@ class AccountTransformer extends AbstractTransformer { private array $accountMeta; private array $accountTypes; - private array $balances; + private array $balanceDifferences; private array $convertedBalances; private array $currencies; private TransactionCurrency $default; private array $lastActivity; /** - * @throws FireflyException + * This method collects meta-data for one or all accounts in the transformer's collection. */ public function collectMetaData(Collection $objects): Collection { - // TODO separate methods - $this->currencies = []; - $this->accountMeta = []; - $this->accountTypes = []; - $this->lastActivity = []; - $this->balances = app('steam')->balancesByAccounts($objects, $this->getDate()); - $this->convertedBalances = app('steam')->balancesByAccountsConverted($objects, $this->getDate()); + $this->currencies = []; + $this->accountMeta = []; + $this->accountTypes = []; + $this->lastActivity = []; + $this->convertedBalances = []; + $this->balanceDifferences = []; - /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class); - $this->default = app('amount')->getDefaultCurrency(); + // get balances of all accounts + $this->getMetaBalances($objects); - // get currencies: - $accountIds = $objects->pluck('id')->toArray(); - // TODO move query to repository - $meta = AccountMeta::whereIn('account_id', $accountIds) - ->whereIn('name', ['currency_id', 'account_role', 'account_number']) - ->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data']) - ; - $currencyIds = $meta->where('name', 'currency_id')->pluck('data')->toArray(); + // get default currency: + $this->getDefaultCurrency(); + + // collect currency and other meta-data: + $this->collectAccountMetaData($objects); - $currencies = $repository->getByIds($currencyIds); - foreach ($currencies as $currency) { - $id = $currency->id; - $this->currencies[$id] = $currency; - } - foreach ($meta as $entry) { - $id = $entry->account_id; - $this->accountMeta[$id][$entry->name] = $entry->data; - } // get account types: - // select accounts.id, account_types.type from account_types left join accounts on accounts.account_type_id = account_types.id; - // TODO move query to repository - $accountTypes = AccountType::leftJoin('accounts', 'accounts.account_type_id', '=', 'account_types.id') - ->whereIn('accounts.id', $accountIds) - ->get(['accounts.id', 'account_types.type']) - ; + $this->collectAccountTypes($objects); - /** @var AccountType $row */ - foreach ($accountTypes as $row) { - $this->accountTypes[$row->id] = (string)config(sprintf('firefly.shortNamesByFullName.%s', $row->type)); + // get last activity: + $this->getLastActivity($objects); + + // TODO add balance difference + if (null !== $this->parameters->get('start') && null !== $this->parameters->get('end')) { + $this->getBalanceDifference($objects, $this->parameters->get('start'), $this->parameters->get('end')); } - // get last activity - // TODO move query to repository - $array = Transaction::whereIn('account_id', $accountIds) - ->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id') - ->groupBy('transactions.account_id') - ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) as date_max')])->toArray() // @phpstan-ignore-line - ; - foreach ($array as $row) { - $this->lastActivity[(int)$row['account_id']] = Carbon::parse($row['date_max'], config('app.timezone')); - } + return $this->sortAccounts($objects); - // TODO needs separate method. - /** @var null|array $sort */ - $sort = $this->parameters->get('sort'); - if (null !== $sort && 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; - }); - } - if ('last_activity' === $column) { - $dates = $this->lastActivity; - $objects = $objects->sort(function (Account $left, Account $right) use ($dates, $direction) { - $leftDate = $dates[$left->id] ?? Carbon::create(1900, 1, 1, 0, 0, 0); - $rightDate = $dates[$right->id] ?? Carbon::create(1900, 1, 1, 0, 0, 0); - if ('asc' === $direction) { - return $leftDate->gt($rightDate) ? 1 : -1; - } - - return $rightDate->gt($leftDate) ? 1 : -1; - }); - } - } - } - - // $objects = $objects->sortByDesc('name'); - return $objects; } private function getDate(): Carbon @@ -171,20 +99,20 @@ class AccountTransformer extends AbstractTransformer */ public function transform(Account $account): array { - $id = $account->id; + $id = $account->id; // various meta - $accountRole = $this->accountMeta[$id]['account_role'] ?? null; - $accountType = $this->accountTypes[$id]; - $order = $account->order; + $accountRole = $this->accountMeta[$id]['account_role'] ?? null; + $accountType = $this->accountTypes[$id]; + $order = $account->order; // no currency? use default - $currency = $this->default; - if (array_key_exists($id, $this->accountMeta) && 0 !== (int)($this->accountMeta[$id]['currency_id'] ?? 0)) { - $currency = $this->currencies[(int)$this->accountMeta[$id]['currency_id']]; + $currency = $this->default; + if (array_key_exists($id, $this->accountMeta) && 0 !== (int) ($this->accountMeta[$id]['currency_id'] ?? 0)) { + $currency = $this->currencies[(int) $this->accountMeta[$id]['currency_id']]; } // amounts and calculation. - $balance = $this->balances[$id] ?? null; + $balance = $this->balances[$id]['balance'] ?? null; $nativeBalance = $this->convertedBalances[$id]['native_balance'] ?? null; // no order for some accounts: @@ -192,23 +120,36 @@ class AccountTransformer extends AbstractTransformer $order = null; } - return [ - 'id' => (string)$account->id, - 'created_at' => $account->created_at->toAtomString(), - 'updated_at' => $account->updated_at->toAtomString(), - 'active' => $account->active, - 'order' => $order, - 'name' => $account->name, - 'iban' => '' === (string)$account->iban ? null : $account->iban, - 'account_number' => $this->accountMeta[$id]['account_number'] ?? null, - 'type' => strtolower($accountType), - 'account_role' => $accountRole, - 'currency_id' => (string)$currency->id, - 'currency_code' => $currency->code, - 'currency_symbol' => $currency->symbol, - 'currency_decimal_places' => $currency->decimal_places, + // balance difference + $diffStart = null; + $diffEnd = null; + $balanceDiff = null; + $nativeBalanceDiff = null; + if (null !== $this->parameters->get('start') && null !== $this->parameters->get('end')) { + $diffStart = $this->parameters->get('start')->toAtomString(); + $diffEnd = $this->parameters->get('end')->toAtomString(); + $balanceDiff = $this->balanceDifferences[$id]['balance'] ?? null; + $nativeBalanceDiff = $this->balanceDifferences[$id]['native_balance'] ?? null; + } - 'native_currency_id' => (string)$this->default->id, + + return [ + 'id' => (string) $account->id, + 'created_at' => $account->created_at->toAtomString(), + 'updated_at' => $account->updated_at->toAtomString(), + 'active' => $account->active, + 'order' => $order, + 'name' => $account->name, + 'iban' => '' === (string) $account->iban ? null : $account->iban, + 'account_number' => $this->accountMeta[$id]['account_number'] ?? null, + 'type' => strtolower($accountType), + 'account_role' => $accountRole, + 'currency_id' => (string) $currency->id, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + + 'native_currency_id' => (string) $this->default->id, 'native_currency_code' => $this->default->code, 'native_currency_symbol' => $this->default->symbol, 'native_currency_decimal_places' => $this->default->decimal_places, @@ -218,6 +159,12 @@ class AccountTransformer extends AbstractTransformer 'native_current_balance' => $nativeBalance, 'current_balance_date' => $this->getDate()->endOfDay()->toAtomString(), + // balance difference + 'balance_difference' => $balanceDiff, + 'native_balance_difference' => $nativeBalanceDiff, + 'balance_difference_start' => $diffStart, + 'balance_difference_end' => $diffEnd, + // more meta 'last_activity' => array_key_exists($id, $this->lastActivity) ? $this->lastActivity[$id]->toAtomString() : null, @@ -241,9 +188,179 @@ class AccountTransformer extends AbstractTransformer 'links' => [ [ 'rel' => 'self', - 'uri' => '/accounts/'.$account->id, + 'uri' => '/accounts/' . $account->id, ], ], ]; } + + private function getMetaBalances(Collection $accounts): void + { + try { + $this->convertedBalances = app('steam')->balancesByAccountsConverted($accounts, $this->getDate()); + } catch (FireflyException $e) { + Log::error($e->getMessage()); + } + } + + private function getDefaultCurrency(): void + { + $this->default = app('amount')->getDefaultCurrency(); + + } + + private function collectAccountMetaData(Collection $accounts): void + { + /** @var CurrencyRepositoryInterface $repository */ + $repository = app(CurrencyRepositoryInterface::class); + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $metaFields = $accountRepository->getMetaValues($accounts, ['currency_id', 'account_role', 'account_number']); + $currencyIds = $metaFields->where('name', 'currency_id')->pluck('data')->toArray(); + + $currencies = $repository->getByIds($currencyIds); + foreach ($currencies as $currency) { + $id = $currency->id; + $this->currencies[$id] = $currency; + } + foreach ($metaFields as $entry) { + $id = $entry->account_id; + $this->accountMeta[$id][$entry->name] = $entry->data; + } + } + + private function collectAccountTypes(Collection $accounts): void + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $accountTypes = $accountRepository->getAccountTypes($accounts); + + /** @var AccountType $row */ + foreach ($accountTypes as $row) { + $this->accountTypes[$row->id] = (string) config(sprintf('firefly.shortNamesByFullName.%s', $row->type)); + } + } + + private function getLastActivity(Collection $accounts): void + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $lastActivity = $accountRepository->getLastActivity($accounts); + foreach ($lastActivity as $row) { + $this->lastActivity[(int) $row['account_id']] = Carbon::parse($row['date_max'], config('app.timezone')); + } + } + + private function sortAccounts(Collection $accounts): Collection + { + /** @var null|array $sort */ + $sort = $this->parameters->get('sort'); + + if (null === $sort || 0 === count($sort)) { + return $accounts; + } + + /** + * @var string $column + * @var string $direction + */ + foreach ($sort as $column => $direction) { + + // account_number + iban + if ('iban' === $column) { + $accounts = $this->sortByIban($accounts, $direction); + } + if ('balance' === $column) { + $accounts = $this->sortByBalance($accounts, $direction); + + } + if ('last_activity' === $column) { + $accounts = $this->sortByLastActivity($accounts, $direction); + } + if ('balance_difference' === $column) { + $accounts = $this->sortByBalanceDifference($accounts, $direction); + } + } + return $accounts; + } + + private function sortByIban(Collection $accounts, string $direction): Collection + { + $meta = $this->accountMeta; + return $accounts->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); + }); + } + + private function sortByBalance(Collection $accounts, string $direction): Collection + { + $balances = $this->convertedBalances; + return $accounts->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; + }); + } + + private function sortByLastActivity(Collection $accounts, string $direction): Collection + { + $dates = $this->lastActivity; + return $accounts->sort(function (Account $left, Account $right) use ($dates, $direction) { + $leftDate = $dates[$left->id] ?? Carbon::create(1900, 1, 1, 0, 0, 0); + $rightDate = $dates[$right->id] ?? Carbon::create(1900, 1, 1, 0, 0, 0); + if ('asc' === $direction) { + return $leftDate->gt($rightDate) ? 1 : -1; + } + + return $rightDate->gt($leftDate) ? 1 : -1; + }); + } + + private function getBalanceDifference(Collection $accounts, Carbon $start, Carbon $end): void + { + // collect balances, start and end for both native and converted. + // yes the b is usually used for boolean by idiots but here it's for balance. + $bStart = []; + $bEnd = []; + try { + $bStart = app('steam')->balancesByAccountsConverted($accounts, $start); + $bEnd = app('steam')->balancesByAccountsConverted($accounts, $end); + } catch (FireflyException $e) { + Log::error($e->getMessage()); + } + /** @var Account $account */ + foreach ($accounts as $account) { + $id = $account->id; + if (array_key_exists($id, $bStart) && array_key_exists($id, $bEnd)) { + $this->balanceDifferences[$id] = [ + 'balance' => bcsub($bEnd[$id]['balance'], $bStart[$id]['balance']), + 'native_balance' => bcsub($bEnd[$id]['native_balance'], $bStart[$id]['native_balance']), + ]; + } + } + + } + + private function sortByBalanceDifference(Collection $accounts, string $direction): Collection { + $balances = $this->balanceDifferences; + return $accounts->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; + }); + } } diff --git a/app/User.php b/app/User.php index f624fa08d1..3c58550c92 100644 --- a/app/User.php +++ b/app/User.php @@ -381,10 +381,7 @@ class User extends Authenticatable $dbRolesTitles = $dbRoles->pluck('title')->toArray(); /** @var Collection $groupMemberships */ - $groupMemberships = $this->groupMemberships() - ->whereIn('user_role_id', $dbRolesIds) - ->where('user_group_id', $userGroup->id)->get() - ; + $groupMemberships = $this->groupMemberships()->whereIn('user_role_id', $dbRolesIds)->where('user_group_id', $userGroup->id)->get(); if (0 === $groupMemberships->count()) { app('log')->error(sprintf( 'User #%d "%s" does not have roles %s in user group #%d "%s"', diff --git a/config/firefly.php b/config/firefly.php index 9b2bedb5c1..16aee29799 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -924,7 +924,7 @@ return [ 'sorting' => [ 'allowed' => [ 'transactions' => ['description', 'amount'], - 'accounts' => ['name', 'active', 'iban', 'balance', 'last_activity'], + 'accounts' => ['name', 'active', 'iban', 'balance', 'last_activity','balance_difference'], ], ], ]; diff --git a/resources/assets/v2/src/pages/accounts/index.js b/resources/assets/v2/src/pages/accounts/index.js index 5dad90d86f..79991ab92b 100644 --- a/resources/assets/v2/src/pages/accounts/index.js +++ b/resources/assets/v2/src/pages/accounts/index.js @@ -77,7 +77,7 @@ let index = function () { sort(column) { this.sortingColumn = column; this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; - const url = './accounts/'+type+'?column='+column+'&direction='+this.sortDirection; + const url = './accounts/' + type + '?column=' + column + '&direction=' + this.sortDirection; window.history.pushState({}, "", url); @@ -140,6 +140,11 @@ let index = function () { this.accounts[index].nameEditorVisible = true; }, loadAccounts() { + + // get start and end from the store: + const start = new Date(window.store.get('start')); + const end = new Date(window.store.get('end')); + this.notifications.wait.show = true; this.notifications.wait.text = i18next.t('firefly.wait_loading_data') this.accounts = []; @@ -147,7 +152,7 @@ let index = function () { // &sorting[0][column]=description&sorting[0][direction]=asc const sorting = [{column: this.sortingColumn, direction: this.sortDirection}]; // one page only.o - (new Get()).index({sorting: sorting, type: type, page: this.page}).then(response => { + (new Get()).index({sorting: sorting, type: type, page: this.page, start: start, end: end}).then(response => { for (let i = 0; i < response.data.data.length; i++) { if (response.data.data.hasOwnProperty(i)) { let current = response.data.data[i]; @@ -165,7 +170,10 @@ let index = function () { native_current_balance: current.attributes.native_current_balance, native_currency_code: current.attributes.native_currency_code, last_activity: null === current.attributes.last_activity ? '' : format(new Date(current.attributes.last_activity), i18next.t('config.month_and_day_fns')), + balance_difference: current.attributes.balance_difference, + native_balance_difference: current.attributes.native_balance_difference }; + console.log(current.attributes.balance_difference); this.accounts.push(account); } } diff --git a/resources/assets/v2/src/pages/dashboard/boxes.js b/resources/assets/v2/src/pages/dashboard/boxes.js index cdd1260fda..cdc98ec02f 100644 --- a/resources/assets/v2/src/pages/dashboard/boxes.js +++ b/resources/assets/v2/src/pages/dashboard/boxes.js @@ -23,6 +23,7 @@ import {format} from "date-fns"; import {getVariable} from "../../store/get-variable.js"; import formatMoney from "../../util/format-money.js"; import {getCacheKey} from "../../support/get-cache-key.js"; +import {cleanupCache} from "../../support/cleanup-cache.js"; let afterPromises = false; export default () => ({ @@ -38,6 +39,7 @@ export default () => ({ const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); const boxesCacheKey = getCacheKey('dashboard-boxes-data', start, end); + cleanupCache(); const cacheValid = window.store.get('cacheValid'); let cachedData = window.store.get(boxesCacheKey); diff --git a/resources/assets/v2/src/support/cleanup-cache.js b/resources/assets/v2/src/support/cleanup-cache.js new file mode 100644 index 0000000000..217d471b13 --- /dev/null +++ b/resources/assets/v2/src/support/cleanup-cache.js @@ -0,0 +1,34 @@ +/* + * load-translations.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 {format} from "date-fns"; +import store from "store"; + +function cleanupCache() { + const localValue = store.get('lastActivity'); + + store.each(function(value, key) { + if(key.startsWith('dcx') && !key.includes(localValue)) { + store.remove(key); + } + }); +} + +export {cleanupCache}; diff --git a/resources/assets/v2/src/support/get-cache-key.js b/resources/assets/v2/src/support/get-cache-key.js index 9d4e5b12c8..2af34f3062 100644 --- a/resources/assets/v2/src/support/get-cache-key.js +++ b/resources/assets/v2/src/support/get-cache-key.js @@ -23,7 +23,7 @@ import store from "store"; function getCacheKey(string, start, end) { const localValue = store.get('lastActivity'); - const cacheKey = format(start, 'y-MM-dd') + '_' + format(end, 'y-MM-dd') + '_' + string + localValue; + const cacheKey = 'dcx' + format(start, 'yMMdd')+ format(end, 'yMMdd') + string + localValue; console.log('getCacheKey: ' + cacheKey); return String(cacheKey); } diff --git a/resources/assets/v2/vite.config.js b/resources/assets/v2/vite.config.js index 9dcbc0e3b6..822231a7fc 100644 --- a/resources/assets/v2/vite.config.js +++ b/resources/assets/v2/vite.config.js @@ -21,6 +21,7 @@ import {defineConfig} from 'vite'; import laravel from 'laravel-vite-plugin'; import manifestSRI from 'vite-plugin-manifest-sri'; +import * as fs from "fs"; const host = '127.0.0.1'; @@ -64,8 +65,8 @@ export default defineConfig({ ], publicDirectory: '../../../public', refresh: true, - }) //, - // manifestSRI(), + }), + manifestSRI(), ], @@ -73,5 +74,12 @@ export default defineConfig({ server: { usePolling: true, host: '10.0.0.15', + // hmr: { + // protocol: 'wss', + // }, + https: { + key: fs.readFileSync(`/sites/vm/tls-certificates/wildcard.sd.internal.key`), + cert: fs.readFileSync(`/sites/vm/tls-certificates/wildcard.sd.internal.crt`), + }, }, }); diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 94c4c7efa1..a423e71e3f 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -2090,6 +2090,7 @@ return [ 'logout' => 'Logout', 'logout_other_sessions' => 'Logout all other sessions', 'toggleNavigation' => 'Toggle navigation', + 'toggle_dropdown' => 'Toggle dropdown', 'searchPlaceholder' => 'Search...', 'version' => 'Version', 'dashboard' => 'Dashboard', diff --git a/resources/views/v2/accounts/index.blade.php b/resources/views/v2/accounts/index.blade.php index d2721fa7e7..e085b7ef7c 100644 --- a/resources/views/v2/accounts/index.blade.php +++ b/resources/views/v2/accounts/index.blade.php @@ -83,14 +83,30 @@ - Balance difference -   + + Balance difference + + + @@ -33,7 +33,7 @@ - - + @@ -163,12 +163,12 @@ @@ -176,7 +176,7 @@ - +   diff --git a/resources/views/v2/partials/layout/topbar.blade.php b/resources/views/v2/partials/layout/topbar.blade.php index 8c44ce0658..02d4d0bf5f 100644 --- a/resources/views/v2/partials/layout/topbar.blade.php +++ b/resources/views/v2/partials/layout/topbar.blade.php @@ -2,7 +2,16 @@ - + + +