mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-10-17 01:42:19 +00:00
Expand accounts page.
This commit is contained in:
@@ -122,6 +122,9 @@ class Controller extends BaseController
|
|||||||
$obj = null;
|
$obj = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(null !== $date && 'end' === $field) {
|
||||||
|
$obj->endOfDay();
|
||||||
|
}
|
||||||
$bag->set($field, $obj);
|
$bag->set($field, $obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
public function index(IndexRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -77,28 +77,6 @@ class IndexController extends Controller
|
|||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
$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)
|
|
||||||
;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -44,14 +44,11 @@ class IndexRequest extends FormRequest
|
|||||||
|
|
||||||
public function getAccountTypes(): array
|
public function getAccountTypes(): array
|
||||||
{
|
{
|
||||||
$type = (string)$this->get('type', 'default');
|
$type = (string) $this->get('type', 'default');
|
||||||
|
|
||||||
return $this->mapAccountTypes($type);
|
return $this->mapAccountTypes($type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all data from the request.
|
|
||||||
*/
|
|
||||||
public function getDate(): Carbon
|
public function getDate(): Carbon
|
||||||
{
|
{
|
||||||
return $this->getCarbonDate('date');
|
return $this->getCarbonDate('date');
|
||||||
@@ -63,7 +60,9 @@ class IndexRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
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',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -41,13 +41,16 @@ class SecureHeaders
|
|||||||
public function handle(Request $request, \Closure $next)
|
public function handle(Request $request, \Closure $next)
|
||||||
{
|
{
|
||||||
// generate and share nonce.
|
// generate and share nonce.
|
||||||
$nonce = base64_encode(random_bytes(16));
|
$nonce = base64_encode(random_bytes(16));
|
||||||
Vite::useCspNonce($nonce);
|
Vite::useCspNonce($nonce);
|
||||||
|
if(class_exists('Barryvdh\Debugbar\Facades\Debugbar')) {
|
||||||
|
\Barryvdh\Debugbar\Facades\Debugbar::getJavascriptRenderer()->setCspNonce($nonce);
|
||||||
|
}
|
||||||
app('view')->share('JS_NONCE', $nonce);
|
app('view')->share('JS_NONCE', $nonce);
|
||||||
|
|
||||||
$response = $next($request);
|
$response = $next($request);
|
||||||
$trackingScriptSrc = $this->getTrackingScriptSource();
|
$trackingScriptSrc = $this->getTrackingScriptSource();
|
||||||
$csp = [
|
$csp = [
|
||||||
"default-src 'none'",
|
"default-src 'none'",
|
||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
sprintf("script-src 'unsafe-eval' 'strict-dynamic' 'nonce-%1s'", $nonce),
|
sprintf("script-src 'unsafe-eval' 'strict-dynamic' 'nonce-%1s'", $nonce),
|
||||||
@@ -55,14 +58,30 @@ class SecureHeaders
|
|||||||
"base-uri 'self'",
|
"base-uri 'self'",
|
||||||
"font-src 'self' data:",
|
"font-src 'self' data:",
|
||||||
sprintf("connect-src 'self' %s", $trackingScriptSrc),
|
sprintf("connect-src 'self' %s", $trackingScriptSrc),
|
||||||
sprintf("img-src 'self' 'nonce-%1s'", $nonce),
|
sprintf("img-src 'self' data: 'nonce-%1s' ", $nonce),
|
||||||
"manifest-src 'self'",
|
"manifest-src 'self'",
|
||||||
];
|
];
|
||||||
|
|
||||||
$route = $request->route();
|
// overrule in development mode
|
||||||
$customUrl = '';
|
if (true === env('IS_LOCAL_DEV')) {
|
||||||
$authGuard = (string)config('firefly.authentication_guard');
|
$csp = [
|
||||||
$logoutUrl = (string)config('firefly.custom_logout_url');
|
"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) {
|
if ('remote_user_guard' === $authGuard && '' !== $logoutUrl) {
|
||||||
$customUrl = $logoutUrl;
|
$customUrl = $logoutUrl;
|
||||||
}
|
}
|
||||||
@@ -71,7 +90,7 @@ class SecureHeaders
|
|||||||
$csp[] = sprintf("form-action 'self' %s", $customUrl);
|
$csp[] = sprintf("form-action 'self' %s", $customUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
$featurePolicies = [
|
$featurePolicies = [
|
||||||
"geolocation 'none'",
|
"geolocation 'none'",
|
||||||
"midi 'none'",
|
"midi 'none'",
|
||||||
// "notifications 'none'",
|
// "notifications 'none'",
|
||||||
@@ -110,8 +129,8 @@ class SecureHeaders
|
|||||||
*/
|
*/
|
||||||
private function getTrackingScriptSource(): string
|
private function getTrackingScriptSource(): string
|
||||||
{
|
{
|
||||||
if ('' !== (string)config('firefly.tracker_site_id') && '' !== (string)config('firefly.tracker_url')) {
|
if ('' !== (string) config('firefly.tracker_site_id') && '' !== (string) config('firefly.tracker_url')) {
|
||||||
return (string)config('firefly.tracker_url');
|
return (string) config('firefly.tracker_url');
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
|
@@ -295,4 +295,15 @@ class UserGroupRepository implements UserGroupRepositoryInterface
|
|||||||
$this->user->user_group_id = $userGroup->id;
|
$this->user->user_group_id = $userGroup->id;
|
||||||
$this->user->save();
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -36,8 +36,12 @@ interface UserGroupRepositoryInterface
|
|||||||
{
|
{
|
||||||
public function destroy(UserGroup $userGroup): void;
|
public function destroy(UserGroup $userGroup): void;
|
||||||
|
|
||||||
|
public function getMembershipsFromGroupId(int $groupId): Collection;
|
||||||
|
|
||||||
public function get(): Collection;
|
public function get(): Collection;
|
||||||
|
|
||||||
|
public function getById(int $id): ?UserGroup;
|
||||||
|
|
||||||
public function getAll(): Collection;
|
public function getAll(): Collection;
|
||||||
|
|
||||||
public function setUser(null|Authenticatable|User $user): void;
|
public function setUser(null|Authenticatable|User $user): void;
|
||||||
|
@@ -27,11 +27,13 @@ namespace FireflyIII\Repositories\UserGroups\Account;
|
|||||||
use FireflyIII\Models\Account;
|
use FireflyIII\Models\Account;
|
||||||
use FireflyIII\Models\AccountMeta;
|
use FireflyIII\Models\AccountMeta;
|
||||||
use FireflyIII\Models\AccountType;
|
use FireflyIII\Models\AccountType;
|
||||||
|
use FireflyIII\Models\Transaction;
|
||||||
use FireflyIII\Models\TransactionCurrency;
|
use FireflyIII\Models\TransactionCurrency;
|
||||||
use FireflyIII\Services\Internal\Update\AccountUpdateService;
|
use FireflyIII\Services\Internal\Update\AccountUpdateService;
|
||||||
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
|
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
|
||||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class AccountRepository
|
* Class AccountRepository
|
||||||
@@ -63,8 +65,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');
|
||||||
@@ -90,7 +91,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');
|
||||||
@@ -114,14 +115,14 @@ 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)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$currencyId = (int)$this->getMetaValue($account, 'currency_id');
|
$currencyId = (int) $this->getMetaValue($account, 'currency_id');
|
||||||
if ($currencyId > 0) {
|
if ($currencyId > 0) {
|
||||||
return TransactionCurrency::find($currencyId);
|
return TransactionCurrency::find($currencyId);
|
||||||
}
|
}
|
||||||
@@ -143,7 +144,7 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (1 === $result->count()) {
|
if (1 === $result->count()) {
|
||||||
return (string)$result->first()->data;
|
return (string) $result->first()->data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -228,7 +229,7 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
|
|
||||||
continue;
|
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));
|
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->order = $index;
|
||||||
$account->save();
|
$account->save();
|
||||||
@@ -240,9 +241,9 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
|
|
||||||
public function getAccountsByType(array $types, ?array $sort = []): Collection
|
public function getAccountsByType(array $types, ?array $sort = []): Collection
|
||||||
{
|
{
|
||||||
$sortable = ['name', 'active']; // TODO yes this is a duplicate array.
|
$sortable = ['name', 'active']; // TODO yes this is a duplicate array.
|
||||||
$res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types);
|
$res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types);
|
||||||
$query = $this->userGroup->accounts();
|
$query = $this->userGroup->accounts();
|
||||||
if (0 !== count($types)) {
|
if (0 !== count($types)) {
|
||||||
$query->accountTypeIn($types);
|
$query->accountTypeIn($types);
|
||||||
}
|
}
|
||||||
@@ -275,12 +276,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);
|
||||||
@@ -305,4 +305,30 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
|
|
||||||
return $service->update($account, $data);
|
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
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -37,6 +37,12 @@ interface AccountRepositoryInterface
|
|||||||
{
|
{
|
||||||
public function countAccounts(array $types): int;
|
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 find(int $accountId): ?Account;
|
||||||
|
|
||||||
public function findByAccountNumber(string $number, array $types): ?Account;
|
public function findByAccountNumber(string $number, array $types): ?Account;
|
||||||
|
@@ -24,12 +24,13 @@ declare(strict_types=1);
|
|||||||
namespace FireflyIII\Support\Http\Api;
|
namespace FireflyIII\Support\Http\Api;
|
||||||
|
|
||||||
use FireflyIII\Enums\UserRoleEnum;
|
use FireflyIII\Enums\UserRoleEnum;
|
||||||
use FireflyIII\Models\GroupMembership;
|
|
||||||
use FireflyIII\Models\UserGroup;
|
use FireflyIII\Models\UserGroup;
|
||||||
|
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
|
||||||
use FireflyIII\User;
|
use FireflyIII\User;
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
use Illuminate\Auth\AuthenticationException;
|
use Illuminate\Auth\AuthenticationException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trait ValidatesUserGroupTrait
|
* Trait ValidatesUserGroupTrait
|
||||||
@@ -42,63 +43,63 @@ trait ValidatesUserGroupTrait
|
|||||||
*/
|
*/
|
||||||
protected function validateUserGroup(Request $request): UserGroup
|
protected function validateUserGroup(Request $request): UserGroup
|
||||||
{
|
{
|
||||||
app('log')->debug(sprintf('validateUserGroup: %s', static::class));
|
Log::debug(sprintf('validateUserGroup: %s', static::class));
|
||||||
if (!auth()->check()) {
|
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();
|
throw new AuthenticationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
$groupId = 0;
|
$groupId = 0;
|
||||||
if (!$request->has('user_group_id')) {
|
if (!$request->has('user_group_id')) {
|
||||||
$groupId = $user->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')) {
|
if ($request->has('user_group_id')) {
|
||||||
$groupId = (int)$request->get('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));
|
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 */
|
if (0 === $memberships->count()) {
|
||||||
$membership = $user->groupMemberships()->where('user_group_id', $groupId)->first();
|
Log::debug(sprintf('validateUserGroup: user has no access to group #%d.', $groupId));
|
||||||
|
|
||||||
if (null === $membership) {
|
throw new AuthorizationException((string) trans('validation.no_access_group'));
|
||||||
app('log')->debug(sprintf('validateUserGroup: user has no access to group #%d.', $groupId));
|
|
||||||
|
|
||||||
throw new AuthorizationException((string)trans('validation.no_access_group'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// need to get the group from the membership:
|
// need to get the group from the membership:
|
||||||
/** @var null|UserGroup $group */
|
$group = $repository->getById($groupId);
|
||||||
$group = $membership->userGroup;
|
|
||||||
if (null === $group) {
|
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));
|
Log::debug(sprintf('validateUserGroup: validate access of user to group #%d ("%s").', $groupId, $group->title));
|
||||||
$roles = property_exists($this, 'acceptedRoles') ? $this->acceptedRoles : [];
|
$roles = property_exists($this, 'acceptedRoles') ? $this->acceptedRoles : [];
|
||||||
if (0 === count($roles)) {
|
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 */
|
/** @var UserRoleEnum $role */
|
||||||
foreach ($roles as $role) {
|
foreach ($roles as $role) {
|
||||||
if ($user->hasRoleInGroupOrOwner($group, $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;
|
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'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,13 +27,12 @@ namespace FireflyIII\Transformers\V2;
|
|||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use FireflyIII\Exceptions\FireflyException;
|
use FireflyIII\Exceptions\FireflyException;
|
||||||
use FireflyIII\Models\Account;
|
use FireflyIII\Models\Account;
|
||||||
use FireflyIII\Models\AccountMeta;
|
|
||||||
use FireflyIII\Models\AccountType;
|
use FireflyIII\Models\AccountType;
|
||||||
use FireflyIII\Models\Transaction;
|
|
||||||
use FireflyIII\Models\TransactionCurrency;
|
use FireflyIII\Models\TransactionCurrency;
|
||||||
|
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
|
||||||
use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface;
|
use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class AccountTransformer
|
* Class AccountTransformer
|
||||||
@@ -42,118 +41,47 @@ class AccountTransformer extends AbstractTransformer
|
|||||||
{
|
{
|
||||||
private array $accountMeta;
|
private array $accountMeta;
|
||||||
private array $accountTypes;
|
private array $accountTypes;
|
||||||
private array $balances;
|
private array $balanceDifferences;
|
||||||
private array $convertedBalances;
|
private array $convertedBalances;
|
||||||
private array $currencies;
|
private array $currencies;
|
||||||
private TransactionCurrency $default;
|
private TransactionCurrency $default;
|
||||||
private array $lastActivity;
|
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
|
public function collectMetaData(Collection $objects): Collection
|
||||||
{
|
{
|
||||||
// TODO separate methods
|
$this->currencies = [];
|
||||||
$this->currencies = [];
|
$this->accountMeta = [];
|
||||||
$this->accountMeta = [];
|
$this->accountTypes = [];
|
||||||
$this->accountTypes = [];
|
$this->lastActivity = [];
|
||||||
$this->lastActivity = [];
|
$this->convertedBalances = [];
|
||||||
$this->balances = app('steam')->balancesByAccounts($objects, $this->getDate());
|
$this->balanceDifferences = [];
|
||||||
$this->convertedBalances = app('steam')->balancesByAccountsConverted($objects, $this->getDate());
|
|
||||||
|
|
||||||
/** @var CurrencyRepositoryInterface $repository */
|
// get balances of all accounts
|
||||||
$repository = app(CurrencyRepositoryInterface::class);
|
$this->getMetaBalances($objects);
|
||||||
$this->default = app('amount')->getDefaultCurrency();
|
|
||||||
|
|
||||||
// get currencies:
|
// get default currency:
|
||||||
$accountIds = $objects->pluck('id')->toArray();
|
$this->getDefaultCurrency();
|
||||||
// TODO move query to repository
|
|
||||||
$meta = AccountMeta::whereIn('account_id', $accountIds)
|
// collect currency and other meta-data:
|
||||||
->whereIn('name', ['currency_id', 'account_role', 'account_number'])
|
$this->collectAccountMetaData($objects);
|
||||||
->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data'])
|
|
||||||
;
|
|
||||||
$currencyIds = $meta->where('name', 'currency_id')->pluck('data')->toArray();
|
|
||||||
|
|
||||||
$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:
|
// get account types:
|
||||||
// select accounts.id, account_types.type from account_types left join accounts on accounts.account_type_id = account_types.id;
|
$this->collectAccountTypes($objects);
|
||||||
// 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'])
|
|
||||||
;
|
|
||||||
|
|
||||||
/** @var AccountType $row */
|
// get last activity:
|
||||||
foreach ($accountTypes as $row) {
|
$this->getLastActivity($objects);
|
||||||
$this->accountTypes[$row->id] = (string)config(sprintf('firefly.shortNamesByFullName.%s', $row->type));
|
|
||||||
|
// 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
|
return $this->sortAccounts($objects);
|
||||||
// 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'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
private function getDate(): Carbon
|
||||||
@@ -171,20 +99,20 @@ 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'] ?? 0)) {
|
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->currencies[(int) $this->accountMeta[$id]['currency_id']];
|
||||||
}
|
}
|
||||||
// amounts and calculation.
|
// amounts and calculation.
|
||||||
$balance = $this->balances[$id] ?? null;
|
$balance = $this->balances[$id]['balance'] ?? null;
|
||||||
$nativeBalance = $this->convertedBalances[$id]['native_balance'] ?? null;
|
$nativeBalance = $this->convertedBalances[$id]['native_balance'] ?? null;
|
||||||
|
|
||||||
// no order for some accounts:
|
// no order for some accounts:
|
||||||
@@ -192,23 +120,36 @@ class AccountTransformer extends AbstractTransformer
|
|||||||
$order = null;
|
$order = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
// balance difference
|
||||||
'id' => (string)$account->id,
|
$diffStart = null;
|
||||||
'created_at' => $account->created_at->toAtomString(),
|
$diffEnd = null;
|
||||||
'updated_at' => $account->updated_at->toAtomString(),
|
$balanceDiff = null;
|
||||||
'active' => $account->active,
|
$nativeBalanceDiff = null;
|
||||||
'order' => $order,
|
if (null !== $this->parameters->get('start') && null !== $this->parameters->get('end')) {
|
||||||
'name' => $account->name,
|
$diffStart = $this->parameters->get('start')->toAtomString();
|
||||||
'iban' => '' === (string)$account->iban ? null : $account->iban,
|
$diffEnd = $this->parameters->get('end')->toAtomString();
|
||||||
'account_number' => $this->accountMeta[$id]['account_number'] ?? null,
|
$balanceDiff = $this->balanceDifferences[$id]['balance'] ?? null;
|
||||||
'type' => strtolower($accountType),
|
$nativeBalanceDiff = $this->balanceDifferences[$id]['native_balance'] ?? null;
|
||||||
'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,
|
|
||||||
|
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_code' => $this->default->code,
|
||||||
'native_currency_symbol' => $this->default->symbol,
|
'native_currency_symbol' => $this->default->symbol,
|
||||||
'native_currency_decimal_places' => $this->default->decimal_places,
|
'native_currency_decimal_places' => $this->default->decimal_places,
|
||||||
@@ -218,6 +159,12 @@ class AccountTransformer extends AbstractTransformer
|
|||||||
'native_current_balance' => $nativeBalance,
|
'native_current_balance' => $nativeBalance,
|
||||||
'current_balance_date' => $this->getDate()->endOfDay()->toAtomString(),
|
'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
|
// more meta
|
||||||
'last_activity' => array_key_exists($id, $this->lastActivity) ? $this->lastActivity[$id]->toAtomString() : null,
|
'last_activity' => array_key_exists($id, $this->lastActivity) ? $this->lastActivity[$id]->toAtomString() : null,
|
||||||
|
|
||||||
@@ -241,9 +188,179 @@ class AccountTransformer extends AbstractTransformer
|
|||||||
'links' => [
|
'links' => [
|
||||||
[
|
[
|
||||||
'rel' => 'self',
|
'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;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -381,10 +381,7 @@ class User extends Authenticatable
|
|||||||
$dbRolesTitles = $dbRoles->pluck('title')->toArray();
|
$dbRolesTitles = $dbRoles->pluck('title')->toArray();
|
||||||
|
|
||||||
/** @var Collection $groupMemberships */
|
/** @var Collection $groupMemberships */
|
||||||
$groupMemberships = $this->groupMemberships()
|
$groupMemberships = $this->groupMemberships()->whereIn('user_role_id', $dbRolesIds)->where('user_group_id', $userGroup->id)->get();
|
||||||
->whereIn('user_role_id', $dbRolesIds)
|
|
||||||
->where('user_group_id', $userGroup->id)->get()
|
|
||||||
;
|
|
||||||
if (0 === $groupMemberships->count()) {
|
if (0 === $groupMemberships->count()) {
|
||||||
app('log')->error(sprintf(
|
app('log')->error(sprintf(
|
||||||
'User #%d "%s" does not have roles %s in user group #%d "%s"',
|
'User #%d "%s" does not have roles %s in user group #%d "%s"',
|
||||||
|
@@ -924,7 +924,7 @@ return [
|
|||||||
'sorting' => [
|
'sorting' => [
|
||||||
'allowed' => [
|
'allowed' => [
|
||||||
'transactions' => ['description', 'amount'],
|
'transactions' => ['description', 'amount'],
|
||||||
'accounts' => ['name', 'active', 'iban', 'balance', 'last_activity'],
|
'accounts' => ['name', 'active', 'iban', 'balance', 'last_activity','balance_difference'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
@@ -77,7 +77,7 @@ let index = function () {
|
|||||||
sort(column) {
|
sort(column) {
|
||||||
this.sortingColumn = column;
|
this.sortingColumn = column;
|
||||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
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);
|
window.history.pushState({}, "", url);
|
||||||
|
|
||||||
@@ -140,6 +140,11 @@ let index = function () {
|
|||||||
this.accounts[index].nameEditorVisible = true;
|
this.accounts[index].nameEditorVisible = true;
|
||||||
},
|
},
|
||||||
loadAccounts() {
|
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.show = true;
|
||||||
this.notifications.wait.text = i18next.t('firefly.wait_loading_data')
|
this.notifications.wait.text = i18next.t('firefly.wait_loading_data')
|
||||||
this.accounts = [];
|
this.accounts = [];
|
||||||
@@ -147,7 +152,7 @@ let index = function () {
|
|||||||
// &sorting[0][column]=description&sorting[0][direction]=asc
|
// &sorting[0][column]=description&sorting[0][direction]=asc
|
||||||
const sorting = [{column: this.sortingColumn, direction: this.sortDirection}];
|
const sorting = [{column: this.sortingColumn, direction: this.sortDirection}];
|
||||||
// one page only.o
|
// 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++) {
|
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];
|
||||||
@@ -165,7 +170,10 @@ let index = function () {
|
|||||||
native_current_balance: current.attributes.native_current_balance,
|
native_current_balance: current.attributes.native_current_balance,
|
||||||
native_currency_code: current.attributes.native_currency_code,
|
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')),
|
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);
|
this.accounts.push(account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,6 +23,7 @@ import {format} from "date-fns";
|
|||||||
import {getVariable} from "../../store/get-variable.js";
|
import {getVariable} from "../../store/get-variable.js";
|
||||||
import formatMoney from "../../util/format-money.js";
|
import formatMoney from "../../util/format-money.js";
|
||||||
import {getCacheKey} from "../../support/get-cache-key.js";
|
import {getCacheKey} from "../../support/get-cache-key.js";
|
||||||
|
import {cleanupCache} from "../../support/cleanup-cache.js";
|
||||||
|
|
||||||
let afterPromises = false;
|
let afterPromises = false;
|
||||||
export default () => ({
|
export default () => ({
|
||||||
@@ -38,6 +39,7 @@ export default () => ({
|
|||||||
const start = new Date(window.store.get('start'));
|
const start = new Date(window.store.get('start'));
|
||||||
const end = new Date(window.store.get('end'));
|
const end = new Date(window.store.get('end'));
|
||||||
const boxesCacheKey = getCacheKey('dashboard-boxes-data', start, end);
|
const boxesCacheKey = getCacheKey('dashboard-boxes-data', start, end);
|
||||||
|
cleanupCache();
|
||||||
|
|
||||||
const cacheValid = window.store.get('cacheValid');
|
const cacheValid = window.store.get('cacheValid');
|
||||||
let cachedData = window.store.get(boxesCacheKey);
|
let cachedData = window.store.get(boxesCacheKey);
|
||||||
|
34
resources/assets/v2/src/support/cleanup-cache.js
Normal file
34
resources/assets/v2/src/support/cleanup-cache.js
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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};
|
@@ -23,7 +23,7 @@ import store from "store";
|
|||||||
|
|
||||||
function getCacheKey(string, start, end) {
|
function getCacheKey(string, start, end) {
|
||||||
const localValue = store.get('lastActivity');
|
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);
|
console.log('getCacheKey: ' + cacheKey);
|
||||||
return String(cacheKey);
|
return String(cacheKey);
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@
|
|||||||
import {defineConfig} from 'vite';
|
import {defineConfig} from 'vite';
|
||||||
import laravel from 'laravel-vite-plugin';
|
import laravel from 'laravel-vite-plugin';
|
||||||
import manifestSRI from 'vite-plugin-manifest-sri';
|
import manifestSRI from 'vite-plugin-manifest-sri';
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
const host = '127.0.0.1';
|
const host = '127.0.0.1';
|
||||||
|
|
||||||
@@ -64,8 +65,8 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
publicDirectory: '../../../public',
|
publicDirectory: '../../../public',
|
||||||
refresh: true,
|
refresh: true,
|
||||||
}) //,
|
}),
|
||||||
// manifestSRI(),
|
manifestSRI(),
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -73,5 +74,12 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
usePolling: true,
|
usePolling: true,
|
||||||
host: '10.0.0.15',
|
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`),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@@ -2090,6 +2090,7 @@ return [
|
|||||||
'logout' => 'Logout',
|
'logout' => 'Logout',
|
||||||
'logout_other_sessions' => 'Logout all other sessions',
|
'logout_other_sessions' => 'Logout all other sessions',
|
||||||
'toggleNavigation' => 'Toggle navigation',
|
'toggleNavigation' => 'Toggle navigation',
|
||||||
|
'toggle_dropdown' => 'Toggle dropdown',
|
||||||
'searchPlaceholder' => 'Search...',
|
'searchPlaceholder' => 'Search...',
|
||||||
'version' => 'Version',
|
'version' => 'Version',
|
||||||
'dashboard' => 'Dashboard',
|
'dashboard' => 'Dashboard',
|
||||||
|
@@ -83,14 +83,30 @@
|
|||||||
<em x-show="sortingColumn === 'last_activity' && sortDirection === 'asc'" class="fa-solid fa-arrow-down-wide-short"></em>
|
<em x-show="sortingColumn === 'last_activity' && sortDirection === 'asc'" class="fa-solid fa-arrow-down-wide-short"></em>
|
||||||
<em x-show="sortingColumn === 'last_activity' && sortDirection === 'desc'" class="fa-solid fa-arrow-up-wide-short"></em>
|
<em x-show="sortingColumn === 'last_activity' && sortDirection === 'desc'" class="fa-solid fa-arrow-up-wide-short"></em>
|
||||||
</td>
|
</td>
|
||||||
<td>Balance difference</td>
|
<td>
|
||||||
<td> </td>
|
<a href="#" x-on:click.prevent="sort('balance_difference')">Balance difference</a>
|
||||||
|
<em x-show="sortingColumn === 'balance_difference' && sortDirection === 'asc'" class="fa-solid fa-arrow-down-wide-short"></em>
|
||||||
|
<em x-show="sortingColumn === 'balance_difference' && sortDirection === 'desc'" class="fa-solid fa-arrow-up-wide-short"></em>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template x-for="(account, index) in accounts" :key="index">
|
<template x-for="(account, index) in accounts" :key="index">
|
||||||
<tr>
|
<tr>
|
||||||
<td>TODO</td>
|
<td>
|
||||||
|
<!-- Example split danger button -->
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<a :href="'./accounts/edit/' + account.id" class="btn btn-sm btn-light"><em class="fa-solid fa-pencil"></em></a>
|
||||||
|
<button type="button" class="btn btn-light dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<span class="visually-hidden">{{ __('firefly.toggle_dropdown') }}</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" :href="'./accounts/show/' + account.id"><em class="fa-solid fa-eye"></em> {{ __('firefly.show') }}</a></li>
|
||||||
|
<li><a class="dropdown-item" :href="'./accounts/reconcile/' + account.id"><em class="fa-solid fa-calculator"></em> {{ __('firefly.reconcile_selected') }}</a></li>
|
||||||
|
<li><a class="dropdown-item" :href="'./accounts/delete/' + account.id"><em class="fa-solid fa-trash"></em> {{ __('firefly.delete') }}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<template x-if="account.active">
|
<template x-if="account.active">
|
||||||
<em class="text-success fa-solid fa-check"></em>
|
<em class="text-success fa-solid fa-check"></em>
|
||||||
@@ -150,8 +166,11 @@
|
|||||||
<td>
|
<td>
|
||||||
<span x-text="account.last_activity"></span>
|
<span x-text="account.last_activity"></span>
|
||||||
</td>
|
</td>
|
||||||
<td>TODO 2 </td>
|
<td>
|
||||||
<td> </td>
|
<template x-if="null !== account.balance_difference">
|
||||||
|
<span x-text="formatMoney(account.balance_difference, account.currency_code)"></span>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@@ -35,7 +35,10 @@
|
|||||||
|
|
||||||
<!-- begin date range drop down -->
|
<!-- begin date range drop down -->
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link daterange-holder" data-bs-toggle="dropdown" href="#"></a>
|
<a class="nav-link daterange-holder d-none d-sm-block" data-bs-toggle="dropdown" href="#"></a>
|
||||||
|
<a class="nav-link daterange-icon d-block d-sm-none" data-bs-toggle="dropdown" href="#">
|
||||||
|
<em class="fa-regular fa-calendar-days"></em>
|
||||||
|
</a>
|
||||||
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-end">
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-end">
|
||||||
<a href="#" class="dropdown-item daterange-current" @click="changeDateRange">
|
<a href="#" class="dropdown-item daterange-current" @click="changeDateRange">
|
||||||
|
|
||||||
|
@@ -14,17 +14,17 @@
|
|||||||
</template>
|
</template>
|
||||||
</h3>
|
</h3>
|
||||||
<template x-if="loading">
|
<template x-if="loading">
|
||||||
<p>
|
<p class="d-none d-xs-block">
|
||||||
<em class="fa-solid fa-spinner fa-spin"></em>
|
<em class="fa-solid fa-spinner fa-spin"></em>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!loading && 0 !== balanceBox.amounts.length">
|
<template x-if="!loading && 0 !== balanceBox.amounts.length">
|
||||||
<p>
|
<p class="d-none d-sm-block">
|
||||||
<a href="{{ route('reports.report.default', ['allAssetAccounts',$start->format('Ymd'),$end->format('Ymd')]) }}">{{ __('firefly.in_out_period') }}</a>
|
<a href="{{ route('reports.report.default', ['allAssetAccounts',$start->format('Ymd'),$end->format('Ymd')]) }}">{{ __('firefly.in_out_period') }}</a>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!loading && 0 === balanceBox.amounts.length">
|
<template x-if="!loading && 0 === balanceBox.amounts.length">
|
||||||
<p>
|
<p class="d-none d-sm-block">
|
||||||
TODO (no money in or out)
|
TODO (no money in or out)
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<i class="fa-solid fa-scale-balanced"></i>
|
<i class="fa-solid fa-scale-balanced"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="small-box-footer hover-footer">
|
<div class="small-box-footer hover-footer d-none d-xl-block">
|
||||||
<template x-if="0 === balanceBox.subtitles.length">
|
<template x-if="0 === balanceBox.subtitles.length">
|
||||||
<span> </span>
|
<span> </span>
|
||||||
</template>
|
</template>
|
||||||
@@ -66,21 +66,21 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="loading">
|
<template x-if="loading">
|
||||||
<p>
|
<p class="d-none d-sm-block">
|
||||||
<em class="fa-solid fa-spinner fa-spin"></em>
|
<em class="fa-solid fa-spinner fa-spin"></em>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!loading && billBox.unpaid.length > 0">
|
<template x-if="!loading && billBox.unpaid.length > 0">
|
||||||
<p><a href="{{ route('bills.index') }}">{{ __('firefly.bills_to_pay') }}</a></p>
|
<p class="d-none d-sm-block"><a href="{{ route('bills.index') }}">{{ __('firefly.bills_to_pay') }}</a></p>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="0 === billBox.unpaid.length && !loading">
|
<template x-if="0 === billBox.unpaid.length && !loading">
|
||||||
<p>TODO No subscriptions are waiting to be paid</p>
|
<p class="d-none d-sm-block">TODO No subscriptions are waiting to be paid</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<span class="small-box-icon">
|
<span class="small-box-icon">
|
||||||
<em class="fa-regular fa-calendar"></em>
|
<em class="fa-regular fa-calendar"></em>
|
||||||
</span>
|
</span>
|
||||||
<span class="small-box-footer">
|
<span class="small-box-footer d-none d-xl-block">
|
||||||
<template x-if="0 === billBox.paid.length">
|
<template x-if="0 === billBox.paid.length">
|
||||||
<span> </span>
|
<span> </span>
|
||||||
</template>
|
</template>
|
||||||
@@ -117,21 +117,21 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<template x-if="loading">
|
<template x-if="loading">
|
||||||
<p>
|
<p class="d-none d-sm-block">
|
||||||
<em class="fa-solid fa-spinner fa-spin"></em>
|
<em class="fa-solid fa-spinner fa-spin"></em>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!loading && 0 !== leftBox.left.length">
|
<template x-if="!loading && 0 !== leftBox.left.length">
|
||||||
<p><a href="{{ route('budgets.index') }}">{{ __('firefly.left_to_spend') }}</a></p>
|
<p class="d-none d-sm-block"><a href="{{ route('budgets.index') }}">{{ __('firefly.left_to_spend') }}</a></p>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!loading && 0 === leftBox.left.length">
|
<template x-if="!loading && 0 === leftBox.left.length">
|
||||||
<p>TODO no money is budgeted in this period</p>
|
<p class="d-none d-sm-block">TODO no money is budgeted in this period</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<span class="small-box-icon">
|
<span class="small-box-icon">
|
||||||
<em class="fa-solid fa-money-check-dollar"></em>
|
<em class="fa-solid fa-money-check-dollar"></em>
|
||||||
</span>
|
</span>
|
||||||
<span class="small-box-footer">
|
<span class="small-box-footer d-none d-xl-block">
|
||||||
<template x-if="0 !== leftBox.perDay.length">
|
<template x-if="0 !== leftBox.perDay.length">
|
||||||
<span>{{ __('firefly.per_day') }}:</span>
|
<span>{{ __('firefly.per_day') }}:</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -163,12 +163,12 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<template x-if="loading">
|
<template x-if="loading">
|
||||||
<p>
|
<p class="d-none d-sm-block">
|
||||||
<em class="fa-solid fa-spinner fa-spin"></em>
|
<em class="fa-solid fa-spinner fa-spin"></em>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!loading">
|
<template x-if="!loading">
|
||||||
<p>
|
<p class="d-none d-sm-block">
|
||||||
<a href="{{ route('reports.report.default', ['allAssetAccounts','currentYearStart','currentYearEnd']) }}">{{ __('firefly.net_worth') }}</a>
|
<a href="{{ route('reports.report.default', ['allAssetAccounts','currentYearStart','currentYearEnd']) }}">{{ __('firefly.net_worth') }}</a>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
<span class="small-box-icon">
|
<span class="small-box-icon">
|
||||||
<i class="fa-solid fa-chart-line"></i>
|
<i class="fa-solid fa-chart-line"></i>
|
||||||
</span>
|
</span>
|
||||||
<span class="small-box-footer">
|
<span class="small-box-footer d-none d-xl-block">
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,7 +2,16 @@
|
|||||||
<a class="nav-link" href="{{ route(Route::current()->getName(), Route::current()->parameters()) }}?force_default_layout=true">
|
<a class="nav-link" href="{{ route(Route::current()->getName(), Route::current()->parameters()) }}?force_default_layout=true">
|
||||||
<i class="fa-solid fa-landmark"></i>
|
<i class="fa-solid fa-landmark"></i>
|
||||||
</a>
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">
|
||||||
|
<em class="fa-solid fa-sliders"></em>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">
|
||||||
|
<em class="fa-solid fa-hat-wizard"></em>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link" data-bs-toggle="dropdown" href="#">
|
<a class="nav-link" data-bs-toggle="dropdown" href="#">
|
||||||
|
Reference in New Issue
Block a user