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