mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-09-25 06:51:08 +00:00
Various code for currency exchange rate support
This commit is contained in:
78
app/Api/V2/Controllers/Model/Bill/SumController.php
Normal file
78
app/Api/V2/Controllers/Model/Bill/SumController.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* SumController.php
|
||||||
|
* Copyright (c) 2022 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace FireflyIII\Api\V2\Controllers\Model\Bill;
|
||||||
|
|
||||||
|
use FireflyIII\Api\V2\Controllers\Controller;
|
||||||
|
use FireflyIII\Api\V2\Request\Generic\DateRequest;
|
||||||
|
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
|
||||||
|
use FireflyIII\Support\Http\Api\ConvertsExchangeRates;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class SumController
|
||||||
|
*/
|
||||||
|
class SumController extends Controller
|
||||||
|
{
|
||||||
|
private BillRepositoryInterface $repository;
|
||||||
|
use ConvertsExchangeRates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware(
|
||||||
|
function ($request, $next) {
|
||||||
|
$this->repository = app(BillRepositoryInterface::class);
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DateRequest $request
|
||||||
|
* @return JsonResponse
|
||||||
|
*/
|
||||||
|
public function unpaid(DateRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$dates = $request->getAll();
|
||||||
|
$result = $this->repository->sumUnpaidInRange($dates['start'], $dates['end']);
|
||||||
|
$converted = $this->cerSum($result);
|
||||||
|
|
||||||
|
// convert to JSON response:
|
||||||
|
return response()->json($converted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DateRequest $request
|
||||||
|
* @return JsonResponse
|
||||||
|
*/
|
||||||
|
public function paid(DateRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$dates = $request->getAll();
|
||||||
|
$result = $this->repository->sumPaidInRange($dates['start'], $dates['end']);
|
||||||
|
$converted = $this->cerSum($result);
|
||||||
|
|
||||||
|
// convert to JSON response:
|
||||||
|
return response()->json($converted);
|
||||||
|
}
|
||||||
|
}
|
65
app/Api/V2/Controllers/Model/Budget/SumController.php
Normal file
65
app/Api/V2/Controllers/Model/Budget/SumController.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* SumController.php
|
||||||
|
* Copyright (c) 2022 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace FireflyIII\Api\V2\Controllers\Model\Budget;
|
||||||
|
|
||||||
|
use FireflyIII\Api\V2\Controllers\Controller;
|
||||||
|
use FireflyIII\Api\V2\Request\Generic\DateRequest;
|
||||||
|
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||||
|
use FireflyIII\Support\Http\Api\ConvertsExchangeRates;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class SumController
|
||||||
|
*/
|
||||||
|
class SumController extends Controller
|
||||||
|
{
|
||||||
|
use ConvertsExchangeRates;
|
||||||
|
|
||||||
|
private BudgetRepositoryInterface $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware(
|
||||||
|
function ($request, $next) {
|
||||||
|
$this->repository = app(BudgetRepositoryInterface::class);
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DateRequest $request
|
||||||
|
* @return JsonResponse
|
||||||
|
*/
|
||||||
|
public function budgeted(DateRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = $request->getAll();
|
||||||
|
$result = $this->repository->budgetedInPeriod($data['start'], $data['end']);
|
||||||
|
$converted = $this->cerSum(array_values($result));
|
||||||
|
|
||||||
|
return response()->json($converted);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
|||||||
namespace FireflyIII\Handlers\Events;
|
namespace FireflyIII\Handlers\Events;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use Database\Seeders\ExchangeRateSeeder;
|
||||||
use Exception;
|
use Exception;
|
||||||
use FireflyIII\Events\ActuallyLoggedIn;
|
use FireflyIII\Events\ActuallyLoggedIn;
|
||||||
use FireflyIII\Events\DetectedNewIPAddress;
|
use FireflyIII\Events\DetectedNewIPAddress;
|
||||||
@@ -75,6 +76,17 @@ class UserEventHandler
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param RegisteredUser $event
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function createExchangeRates(RegisteredUser $event): bool {
|
||||||
|
$seeder = new ExchangeRateSeeder;
|
||||||
|
$seeder->run();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fires to see if a user is admin.
|
* Fires to see if a user is admin.
|
||||||
*
|
*
|
||||||
|
@@ -24,41 +24,11 @@ namespace FireflyIII\Models;
|
|||||||
|
|
||||||
use Eloquent;
|
use Eloquent;
|
||||||
use FireflyIII\User;
|
use FireflyIII\User;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FireflyIII\Models\CurrencyExchangeRate
|
* Class CurrencyExchangeRate
|
||||||
*
|
|
||||||
* @property int $id
|
|
||||||
* @property Carbon|null $created_at
|
|
||||||
* @property Carbon|null $updated_at
|
|
||||||
* @property string|null $deleted_at
|
|
||||||
* @property int $user_id
|
|
||||||
* @property int $from_currency_id
|
|
||||||
* @property int $to_currency_id
|
|
||||||
* @property Carbon $date
|
|
||||||
* @property string $rate
|
|
||||||
* @property string|null $user_rate
|
|
||||||
* @property-read TransactionCurrency $fromCurrency
|
|
||||||
* @property-read TransactionCurrency $toCurrency
|
|
||||||
* @property-read User $user
|
|
||||||
* @method static Builder|CurrencyExchangeRate newModelQuery()
|
|
||||||
* @method static Builder|CurrencyExchangeRate newQuery()
|
|
||||||
* @method static Builder|CurrencyExchangeRate query()
|
|
||||||
* @method static Builder|CurrencyExchangeRate whereCreatedAt($value)
|
|
||||||
* @method static Builder|CurrencyExchangeRate whereDate($value)
|
|
||||||
* @method static Builder|CurrencyExchangeRate whereDeletedAt($value)
|
|
||||||
* @method static Builder|CurrencyExchangeRate whereFromCurrencyId($value)
|
|
||||||
* @method static Builder|CurrencyExchangeRate whereId($value)
|
|
||||||
* @method static Builder|CurrencyExchangeRate whereRate($value)
|
|
||||||
* @method static Builder|CurrencyExchangeRate whereToCurrencyId($value)
|
|
||||||
* @method static Builder|CurrencyExchangeRate whereUpdatedAt($value)
|
|
||||||
* @method static Builder|CurrencyExchangeRate whereUserId($value)
|
|
||||||
* @method static Builder|CurrencyExchangeRate whereUserRate($value)
|
|
||||||
* @mixin Eloquent
|
|
||||||
*/
|
*/
|
||||||
class CurrencyExchangeRate extends Model
|
class CurrencyExchangeRate extends Model
|
||||||
{
|
{
|
||||||
@@ -72,6 +42,7 @@ class CurrencyExchangeRate extends Model
|
|||||||
'to_currency_id' => 'int',
|
'to_currency_id' => 'int',
|
||||||
'date' => 'datetime',
|
'date' => 'datetime',
|
||||||
];
|
];
|
||||||
|
protected $fillable = ['user_id', 'from_currency_id', 'to_currency_id', 'date', 'rate'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @codeCoverageIgnore
|
* @codeCoverageIgnore
|
||||||
|
@@ -83,6 +83,9 @@ class Preference extends Model
|
|||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
/** @var Preference|null $preference */
|
/** @var Preference|null $preference */
|
||||||
$preference = $user->preferences()->where('name', $value)->first();
|
$preference = $user->preferences()->where('name', $value)->first();
|
||||||
|
if (null === $preference) {
|
||||||
|
$preference = $user->preferences()->where('id', (int) $value)->first();
|
||||||
|
}
|
||||||
if (null !== $preference) {
|
if (null !== $preference) {
|
||||||
return $preference;
|
return $preference;
|
||||||
}
|
}
|
||||||
|
@@ -69,6 +69,7 @@ class EventServiceProvider extends ServiceProvider
|
|||||||
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail',
|
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail',
|
||||||
'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole',
|
'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole',
|
||||||
'FireflyIII\Handlers\Events\UserEventHandler@createGroupMembership',
|
'FireflyIII\Handlers\Events\UserEventHandler@createGroupMembership',
|
||||||
|
'FireflyIII\Handlers\Events\UserEventHandler@createExchangeRates',
|
||||||
],
|
],
|
||||||
// is a User related event.
|
// is a User related event.
|
||||||
Login::class => [
|
Login::class => [
|
||||||
|
@@ -33,6 +33,7 @@ use FireflyIII\Models\TransactionCurrency;
|
|||||||
use FireflyIII\User;
|
use FireflyIII\User;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use JsonException;
|
||||||
use Log;
|
use Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -304,7 +305,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface
|
|||||||
*
|
*
|
||||||
* @return BudgetLimit
|
* @return BudgetLimit
|
||||||
* @throws FireflyException
|
* @throws FireflyException
|
||||||
* @throws \JsonException
|
* @throws JsonException
|
||||||
*/
|
*/
|
||||||
public function store(array $data): BudgetLimit
|
public function store(array $data): BudgetLimit
|
||||||
{
|
{
|
||||||
@@ -354,7 +355,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface
|
|||||||
*
|
*
|
||||||
* @return BudgetLimit
|
* @return BudgetLimit
|
||||||
* @throws FireflyException
|
* @throws FireflyException
|
||||||
* @throws \JsonException
|
* @throws JsonException
|
||||||
*/
|
*/
|
||||||
public function update(BudgetLimit $budgetLimit, array $data): BudgetLimit
|
public function update(BudgetLimit $budgetLimit, array $data): BudgetLimit
|
||||||
{
|
{
|
||||||
|
@@ -39,6 +39,7 @@ use FireflyIII\Services\Internal\Destroy\BudgetDestroyService;
|
|||||||
use FireflyIII\User;
|
use FireflyIII\User;
|
||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use JsonException;
|
||||||
use Log;
|
use Log;
|
||||||
use Storage;
|
use Storage;
|
||||||
|
|
||||||
@@ -80,6 +81,60 @@ class BudgetRepository implements BudgetRepositoryInterface
|
|||||||
return $search->take($limit)->get();
|
return $search->take($limit)->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function budgetedInPeriod(Carbon $start, Carbon $end): array
|
||||||
|
{
|
||||||
|
Log::debug(sprintf('Now in budgetedInPeriod("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d')));
|
||||||
|
$return = [];
|
||||||
|
/** @var BudgetLimitRepository $limitRepository */
|
||||||
|
$limitRepository = app(BudgetLimitRepository::class);
|
||||||
|
$limitRepository->setUser($this->user);
|
||||||
|
$budgets = $this->getActiveBudgets();
|
||||||
|
/** @var Budget $budget */
|
||||||
|
foreach ($budgets as $budget) {
|
||||||
|
Log::debug(sprintf('Budget #%d: "%s"', $budget->id, $budget->name));
|
||||||
|
$limits = $limitRepository->getBudgetLimits($budget, $start, $end);
|
||||||
|
/** @var BudgetLimit $limit */
|
||||||
|
foreach ($limits as $limit) {
|
||||||
|
Log::debug(sprintf('Budget limit #%d', $limit->id));
|
||||||
|
$currency = $limit->transactionCurrency;
|
||||||
|
$return[$currency->id] = $return[$currency->id] ?? [
|
||||||
|
'id' => (string) $currency->id,
|
||||||
|
'name' => $currency->name,
|
||||||
|
'symbol' => $currency->symbol,
|
||||||
|
'code' => $currency->code,
|
||||||
|
'decimal_places' => $currency->decimal_places,
|
||||||
|
'sum' => '0',
|
||||||
|
];
|
||||||
|
// same period
|
||||||
|
if ($limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end)) {
|
||||||
|
$return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], (string) $limit->amount);
|
||||||
|
Log::debug(sprintf('Add full amount [1]: %s', $limit->amount));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// limit is inside of date range
|
||||||
|
if ($start->lte($limit->start_date) && $end->gte($limit->end_date)) {
|
||||||
|
$return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], (string) $limit->amount);
|
||||||
|
Log::debug(sprintf('Add full amount [2]: %s', $limit->amount));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$total = $limit->start_date->diffInDays($limit->end_date) + 1; // include the day itself.
|
||||||
|
$days = $this->daysInOverlap($limit, $start, $end);
|
||||||
|
$amount = bcmul(bcdiv((string) $limit->amount, (string) $total), (string) $days);
|
||||||
|
$return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], $amount);
|
||||||
|
Log::debug(sprintf('Amount per day: %s (%s over %d days). Total amount for %d days: %s',
|
||||||
|
bcdiv((string) $limit->amount, (string) $total),
|
||||||
|
$limit->amount,
|
||||||
|
$total,
|
||||||
|
$days,
|
||||||
|
$amount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
@@ -335,7 +390,7 @@ class BudgetRepository implements BudgetRepositoryInterface
|
|||||||
*
|
*
|
||||||
* @return Budget
|
* @return Budget
|
||||||
* @throws FireflyException
|
* @throws FireflyException
|
||||||
* @throws \JsonException
|
* @throws JsonException
|
||||||
*/
|
*/
|
||||||
public function store(array $data): Budget
|
public function store(array $data): Budget
|
||||||
{
|
{
|
||||||
@@ -551,7 +606,7 @@ class BudgetRepository implements BudgetRepositoryInterface
|
|||||||
* @param Budget $budget
|
* @param Budget $budget
|
||||||
* @param array $data
|
* @param array $data
|
||||||
* @throws FireflyException
|
* @throws FireflyException
|
||||||
* @throws \JsonException
|
* @throws JsonException
|
||||||
*/
|
*/
|
||||||
private function updateAutoBudget(Budget $budget, array $data): void
|
private function updateAutoBudget(Budget $budget, array $data): void
|
||||||
{
|
{
|
||||||
@@ -597,4 +652,42 @@ class BudgetRepository implements BudgetRepositoryInterface
|
|||||||
|
|
||||||
$autoBudget->save();
|
$autoBudget->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many days of this budget limit are between start and end?
|
||||||
|
*
|
||||||
|
* @param BudgetLimit $limit
|
||||||
|
* @param Carbon $start
|
||||||
|
* @param Carbon $end
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function daysInOverlap(BudgetLimit $limit, Carbon $start, Carbon $end): int
|
||||||
|
{
|
||||||
|
// start1 = $start
|
||||||
|
// start2 = $limit->start_date
|
||||||
|
// start1 = $end
|
||||||
|
// start2 = $limit->end_date
|
||||||
|
|
||||||
|
// limit is larger than start and end (inclusive)
|
||||||
|
// |-----------|
|
||||||
|
// |----------------|
|
||||||
|
if ($start->gte($limit->start_date) && $end->lte($limit->end_date)) {
|
||||||
|
return $start->diffInDays($end) + 1; // add one day
|
||||||
|
}
|
||||||
|
// limit starts earlier and limit ends first:
|
||||||
|
// |-----------|
|
||||||
|
// |-------|
|
||||||
|
if ($limit->start_date->lte($start) && $limit->end_date->lte($end)) {
|
||||||
|
// return days in the range $start-$limit_end
|
||||||
|
return $start->diffInDays($limit->end_date) + 1; // add one day, the day itself
|
||||||
|
}
|
||||||
|
// limit starts later and limit ends earlier
|
||||||
|
// |-----------|
|
||||||
|
// |-------|
|
||||||
|
if ($limit->start_date->gte($start) && $limit->end_date->gte($end)) {
|
||||||
|
// return days in the range $limit_start - $end
|
||||||
|
return $limit->start_date->diffInDays($end) + 1; // add one day, the day itself
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -34,6 +34,7 @@ use Illuminate\Support\Collection;
|
|||||||
*/
|
*/
|
||||||
interface BudgetRepositoryInterface
|
interface BudgetRepositoryInterface
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $query
|
* @param string $query
|
||||||
* @param int $limit
|
* @param int $limit
|
||||||
@@ -50,6 +51,15 @@ interface BudgetRepositoryInterface
|
|||||||
*/
|
*/
|
||||||
public function budgetStartsWith(string $query, int $limit): Collection;
|
public function budgetStartsWith(string $query, int $limit): Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amount that is budgeted in a period.
|
||||||
|
*
|
||||||
|
* @param Carbon $start
|
||||||
|
* @param Carbon $end
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function budgetedInPeriod(Carbon $start, Carbon $end): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
|
202
app/Support/Http/Api/ConvertsExchangeRates.php
Normal file
202
app/Support/Http/Api/ConvertsExchangeRates.php
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* ConvertsExchangeRates.php
|
||||||
|
* Copyright (c) 2022 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace FireflyIII\Support\Http\Api;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use FireflyIII\Models\CurrencyExchangeRate;
|
||||||
|
use FireflyIII\Models\TransactionCurrency;
|
||||||
|
use Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trait ConvertsExchangeRates
|
||||||
|
*/
|
||||||
|
trait ConvertsExchangeRates
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* For a sum of entries, get the exchange rate to the native currency of
|
||||||
|
* the user.
|
||||||
|
* @param array $entries
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function cerSum(array $entries): array
|
||||||
|
{
|
||||||
|
/** @var TransactionCurrency $native */
|
||||||
|
$native = app('amount')->getDefaultCurrency();
|
||||||
|
$return = [];
|
||||||
|
/** @var array $entry */
|
||||||
|
foreach ($entries as $entry) {
|
||||||
|
$currency = $this->getCurrency((int) $entry['id']);
|
||||||
|
if ($currency->id !== $native->id) {
|
||||||
|
$amount = $this->convertAmount($entry['sum'], $currency, $native);
|
||||||
|
$entry['native_sum'] = $amount;
|
||||||
|
$entry['native_id'] = (string) $native->id;
|
||||||
|
$entry['native_name'] = $native->name;
|
||||||
|
$entry['native_symbol'] = $native->symbol;
|
||||||
|
$entry['native_code'] = $native->code;
|
||||||
|
$entry['native_decimal_places'] = $native->decimal_places;
|
||||||
|
}
|
||||||
|
if ($currency->id === $native->id) {
|
||||||
|
$entry['native_sum'] = $entry['sum'];
|
||||||
|
$entry['native_id'] = (string) $native->id;
|
||||||
|
$entry['native_name'] = $native->name;
|
||||||
|
$entry['native_symbol'] = $native->symbol;
|
||||||
|
$entry['native_code'] = $native->code;
|
||||||
|
$entry['native_decimal_places'] = $native->decimal_places;
|
||||||
|
}
|
||||||
|
$return[] = $entry;
|
||||||
|
|
||||||
|
}
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $currencyId
|
||||||
|
* @return TransactionCurrency
|
||||||
|
*/
|
||||||
|
private function getCurrency(int $currencyId): TransactionCurrency
|
||||||
|
{
|
||||||
|
$result = TransactionCurrency::find($currencyId);
|
||||||
|
if (null === $result) {
|
||||||
|
return app('amount')->getDefaultCurrency();
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $amount
|
||||||
|
* @param TransactionCurrency $from
|
||||||
|
* @param TransactionCurrency $to
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function convertAmount(string $amount, TransactionCurrency $from, TransactionCurrency $to, ?Carbon $date = null): string
|
||||||
|
{
|
||||||
|
Log::debug(sprintf('Converting %s from %s to %s', $amount, $from->code, $to->code));
|
||||||
|
$date = $date ?? Carbon::now();
|
||||||
|
$rate = $this->getRate($from, $to, $date);
|
||||||
|
|
||||||
|
return bcmul($amount, $rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TransactionCurrency $from
|
||||||
|
* @param TransactionCurrency $to
|
||||||
|
* @param Carbon $date
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
|
||||||
|
{
|
||||||
|
Log::debug(sprintf('getRate(%s, %s, "%s")', $from->code, $to->code, $date->format('Y-m-d')));
|
||||||
|
/** @var CurrencyExchangeRate $result */
|
||||||
|
$result = auth()->user()
|
||||||
|
->currencyExchangeRates()
|
||||||
|
->where('from_currency_id', $from->id)
|
||||||
|
->where('to_currency_id', $to->id)
|
||||||
|
->where('date', '<=', $date->format('Y-m-d'))
|
||||||
|
->orderBy('date', 'DESC')
|
||||||
|
->first();
|
||||||
|
if (null !== $result) {
|
||||||
|
$rate = (string) $result->rate;
|
||||||
|
Log::debug(sprintf('Rate is %s', $rate));
|
||||||
|
return $rate;
|
||||||
|
}
|
||||||
|
// no result. perhaps the other way around?
|
||||||
|
/** @var CurrencyExchangeRate $result */
|
||||||
|
$result = auth()->user()
|
||||||
|
->currencyExchangeRates()
|
||||||
|
->where('from_currency_id', $to->id)
|
||||||
|
->where('to_currency_id', $from->id)
|
||||||
|
->where('date', '<=', $date->format('Y-m-d'))
|
||||||
|
->orderBy('date', 'DESC')
|
||||||
|
->first();
|
||||||
|
if (null !== $result) {
|
||||||
|
$rate = bcdiv('1', (string) $result->rate);
|
||||||
|
Log::debug(sprintf('Reversed rate is %s', $rate));
|
||||||
|
return $rate;
|
||||||
|
}
|
||||||
|
// try euro rates
|
||||||
|
$result1 = $this->getEuroRate($from, $date);
|
||||||
|
if ('0' === $result1) {
|
||||||
|
Log::debug(sprintf('No exchange rate between EUR and %s', $from->code));
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
$result2 = $this->getEuroRate($to, $date);
|
||||||
|
if ('0' === $result2) {
|
||||||
|
Log::debug(sprintf('No exchange rate between EUR and %s', $to->code));
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
// still need to inverse rate 2:
|
||||||
|
$result2 = bcdiv('1', $result2);
|
||||||
|
$rate = bcmul($result1, $result2);
|
||||||
|
Log::debug(sprintf('Rate %s to EUR is %s', $from->code, $result1));
|
||||||
|
Log::debug(sprintf('Rate EUR to %s is %s', $to->code, $result2));
|
||||||
|
Log::debug(sprintf('Rate for %s to %s is %s', $from->code, $to->code, $rate));
|
||||||
|
return $rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TransactionCurrency $currency
|
||||||
|
* @param Carbon $date
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getEuroRate(TransactionCurrency $currency, Carbon $date): string
|
||||||
|
{
|
||||||
|
Log::debug(sprintf('Find rate for %s to Euro', $currency->code));
|
||||||
|
$euro = TransactionCurrency::whereCode('EUR')->first();
|
||||||
|
if (null === $euro) {
|
||||||
|
Log::warning('Cannot do indirect conversion without EUR.');
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// try one way:
|
||||||
|
/** @var CurrencyExchangeRate $result */
|
||||||
|
$result = auth()->user()
|
||||||
|
->currencyExchangeRates()
|
||||||
|
->where('from_currency_id', $currency->id)
|
||||||
|
->where('to_currency_id', $euro->id)
|
||||||
|
->where('date', '<=', $date->format('Y-m-d'))
|
||||||
|
->orderBy('date', 'DESC')
|
||||||
|
->first();
|
||||||
|
if (null !== $result) {
|
||||||
|
$rate = (string) $result->rate;
|
||||||
|
Log::debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate));
|
||||||
|
return $rate;
|
||||||
|
}
|
||||||
|
// try the other way around and inverse it.
|
||||||
|
/** @var CurrencyExchangeRate $result */
|
||||||
|
$result = auth()->user()
|
||||||
|
->currencyExchangeRates()
|
||||||
|
->where('from_currency_id', $euro->id)
|
||||||
|
->where('to_currency_id', $currency->id)
|
||||||
|
->where('date', '<=', $date->format('Y-m-d'))
|
||||||
|
->orderBy('date', 'DESC')
|
||||||
|
->first();
|
||||||
|
if (null !== $result) {
|
||||||
|
$rate = bcdiv('1', (string) $result->rate);
|
||||||
|
Log::debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate));
|
||||||
|
return $rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::debug(sprintf('No rate for %s to EUR.', $currency->code));
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
68
config/cer.php
Normal file
68
config/cer.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* default_cer.php
|
||||||
|
* Copyright (c) 2022 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
return [
|
||||||
|
// if currencies are added, default rates must be added as well!
|
||||||
|
// last exchange rate update: 6-6-2022
|
||||||
|
// source: https://www.xe.com/currencyconverter/
|
||||||
|
'date' => '2022-06-06',
|
||||||
|
'rates' => [
|
||||||
|
// europa
|
||||||
|
['EUR', 'HUF', 387.9629],
|
||||||
|
['EUR', 'GBP', 0.85420754],
|
||||||
|
['EUR', 'UAH', 31.659752],
|
||||||
|
['EUR', 'PLN', 4.581788],
|
||||||
|
['EUR', 'TRY', 17.801397],
|
||||||
|
['EUR', 'DKK', 7.4389753],
|
||||||
|
|
||||||
|
// Americas
|
||||||
|
['EUR', 'USD', 1.0722281],
|
||||||
|
['EUR', 'BRL', 5.0973173],
|
||||||
|
['EUR', 'CAD', 1.3459969],
|
||||||
|
['EUR', 'MXN', 20.899824],
|
||||||
|
|
||||||
|
// Oceania currencies
|
||||||
|
['EUR', 'IDR', 15466.299],
|
||||||
|
['EUR', 'AUD', 1.4838549],
|
||||||
|
['EUR', 'NZD', 1.6425829],
|
||||||
|
|
||||||
|
// africa
|
||||||
|
['EUR', 'EGP', 19.99735],
|
||||||
|
['EUR', 'MAD', 10.573307],
|
||||||
|
['EUR', 'ZAR', 16.413167],
|
||||||
|
|
||||||
|
// asia
|
||||||
|
['EUR', 'JPY', 140.15257],
|
||||||
|
['EUR', 'RMB', 7.1194265],
|
||||||
|
['EUR', 'RUB', 66.000895],
|
||||||
|
['EUR', 'INR', 83.220481],
|
||||||
|
|
||||||
|
// int
|
||||||
|
['EUR', 'XBT', 0, 00003417],
|
||||||
|
['EUR', 'BCH', 0.00573987],
|
||||||
|
['EUR', 'ETH', 0, 00056204],
|
||||||
|
|
||||||
|
['EUR', 'ILS', 3.5712508],
|
||||||
|
['EUR', 'CHF', 1.0323891],
|
||||||
|
['EUR', 'HRK', 7.5220845],
|
||||||
|
],
|
||||||
|
];
|
@@ -41,5 +41,6 @@ class DatabaseSeeder extends Seeder
|
|||||||
$this->call(LinkTypeSeeder::class);
|
$this->call(LinkTypeSeeder::class);
|
||||||
$this->call(ConfigSeeder::class);
|
$this->call(ConfigSeeder::class);
|
||||||
$this->call(UserRoleSeeder::class);
|
$this->call(UserRoleSeeder::class);
|
||||||
|
$this->call(ExchangeRateSeeder::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
117
database/seeders/ExchangeRateSeeder.php
Normal file
117
database/seeders/ExchangeRateSeeder.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* ExchangeRateSeeder.php
|
||||||
|
* Copyright (c) 2022 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use FireflyIII\Models\CurrencyExchangeRate;
|
||||||
|
use FireflyIII\Models\TransactionCurrency;
|
||||||
|
use FireflyIII\User;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ExchangeRateSeeder
|
||||||
|
*/
|
||||||
|
class ExchangeRateSeeder extends Seeder
|
||||||
|
{
|
||||||
|
private Collection $users;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$count = User::count();
|
||||||
|
if (0 === $count) {
|
||||||
|
Log::debug('Will not seed exchange rates yet.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$users = User::get();
|
||||||
|
$date = config('cer.date');
|
||||||
|
$rates = config('cer.rates');
|
||||||
|
$usable = [];
|
||||||
|
foreach ($rates as $rate) {
|
||||||
|
$from = $this->getCurrency($rate[0]);
|
||||||
|
$to = $this->getCurrency($rate[1]);
|
||||||
|
if (null !== $from && null !== $to) {
|
||||||
|
$usable[] = [$from, $to, $rate[2]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($rates, $from, $to, $rate);
|
||||||
|
|
||||||
|
/** @var User $user */
|
||||||
|
foreach ($users as $user) {
|
||||||
|
foreach ($usable as $rate) {
|
||||||
|
if (!$this->hasRate($user, $rate[0], $rate[1], $date)) {
|
||||||
|
$this->addRate($user, $rate[0], $rate[1], $date, $rate[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $code
|
||||||
|
* @return TransactionCurrency|null
|
||||||
|
*/
|
||||||
|
private function getCurrency(string $code): ?TransactionCurrency
|
||||||
|
{
|
||||||
|
return TransactionCurrency::whereNull('deleted_at')->where('code', $code)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param User $user
|
||||||
|
* @param TransactionCurrency $from
|
||||||
|
* @param TransactionCurrency $to
|
||||||
|
* @param string $date
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function hasRate(User $user, TransactionCurrency $from, TransactionCurrency $to, string $date): bool
|
||||||
|
{
|
||||||
|
return $user->currencyExchangeRates()
|
||||||
|
->where('from_currency_id', $from->id)
|
||||||
|
->where('to_currency_id', $to->id)
|
||||||
|
->where('date', $date)
|
||||||
|
->count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param User $user
|
||||||
|
* @param TransactionCurrency $from
|
||||||
|
* @param TransactionCurrency $to
|
||||||
|
* @param string $date
|
||||||
|
* @param float $rate
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function addRate(User $user, TransactionCurrency $from, TransactionCurrency $to, string $date, float $rate): void
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
CurrencyExchangeRate::create(
|
||||||
|
[
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'from_currency_id' => $from->id,
|
||||||
|
'to_currency_id' => $to->id,
|
||||||
|
'date' => $date,
|
||||||
|
'rate' => $rate,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -24,6 +24,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import {defineComponent} from 'vue';
|
import {defineComponent} from 'vue';
|
||||||
import Preferences from "./api/preferences";
|
import Preferences from "./api/preferences";
|
||||||
|
import Prefs from "./api/v2/preferences";
|
||||||
import Currencies from "./api/currencies";
|
import Currencies from "./api/currencies";
|
||||||
import {useFireflyIIIStore} from 'stores/fireflyiii'
|
import {useFireflyIIIStore} from 'stores/fireflyiii'
|
||||||
|
|
||||||
@@ -74,10 +75,22 @@ export default defineComponent(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getLocale = function () {
|
||||||
|
return (new Prefs).get('locale').then(data => {
|
||||||
|
const locale = data.data.data.attributes.data.replace('_','-');
|
||||||
|
|
||||||
|
ffStore.setLocale(locale);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error('Could not load locale.')
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
getDefaultCurrency().then(() => {
|
getDefaultCurrency().then(() => {
|
||||||
getViewRange();
|
getViewRange();
|
||||||
getListPageSize();
|
getListPageSize();
|
||||||
|
getLocale();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
38
frontend/src/api/v2/budgets/sum.js
vendored
Normal file
38
frontend/src/api/v2/budgets/sum.js
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* list.js
|
||||||
|
* Copyright (c) 2022 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 {api} from "boot/axios";
|
||||||
|
import {format} from "date-fns";
|
||||||
|
|
||||||
|
export default class Sum {
|
||||||
|
budgeted(start, end) {
|
||||||
|
let url = 'api/v2/budgets/sum/budgeted';
|
||||||
|
let startStr = format(start, 'y-MM-dd');
|
||||||
|
let endStr = format(end, 'y-MM-dd');
|
||||||
|
return api.get(url, {params: {start: startStr, end: endStr}});
|
||||||
|
}
|
||||||
|
|
||||||
|
// /*paid(start, end) {
|
||||||
|
// let url = 'api/v2/bills/sum/paid';
|
||||||
|
// let startStr = format(start, 'y-MM-dd');
|
||||||
|
// let endStr = format(end, 'y-MM-dd');
|
||||||
|
// return api.get(url, {params: {start: startStr, end: endStr}});
|
||||||
|
// }*/
|
||||||
|
}
|
30
frontend/src/api/v2/preferences/index.js
vendored
Normal file
30
frontend/src/api/v2/preferences/index.js
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* basic.js
|
||||||
|
* Copyright (c) 2021 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 {api} from "boot/axios";
|
||||||
|
|
||||||
|
export default class Preferences {
|
||||||
|
get(name) {
|
||||||
|
return api.get('/api/v2/preferences/' + name);
|
||||||
|
}
|
||||||
|
// postByName(name, value) {
|
||||||
|
// return api.post('/api/v1/preferences', {name: name, data: value});
|
||||||
|
// }
|
||||||
|
}
|
@@ -26,7 +26,7 @@
|
|||||||
<q-card bordered>
|
<q-card bordered>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>{{ $t('firefly.bills_to_pay') }}</q-item-label>
|
<q-item-label><strong>{{ $t('firefly.bills_to_pay') }}</strong></q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-separator/>
|
<q-separator/>
|
||||||
@@ -41,36 +41,22 @@
|
|||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-separator vertical/>
|
<q-separator vertical/>
|
||||||
<q-card-section>
|
<q-card-section v-if="0 === unpaid.length && 0 === paid.length">
|
||||||
{{ $t('firefly.bills_to_pay') }}:
|
You have no bills
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section v-if="unpaid.length > 0 || paid.length > 0">
|
||||||
|
<span :title="formatAmount(this.currency, this.unpaidAmount)">{{ $t('firefly.bills_to_pay') }}</span>:
|
||||||
<span v-for="(bill, index) in unpaid">
|
<span v-for="(bill, index) in unpaid">
|
||||||
{{ formatAmount(bill.code, bill.sum) }}
|
<span v-if="bill.native">(</span>{{ formatAmount(bill.code, bill.sum) }}<span
|
||||||
<span v-if="index+1 !== unpaid.length">, </span>
|
v-if="bill.native">)</span><span v-if="index+1 !== unpaid.length">, </span></span>
|
||||||
</span>
|
|
||||||
<br/>
|
<br/>
|
||||||
{{ $t('firefly.bills_paid') }}:
|
<span :title="formatAmount(this.currency, this.paidAmount)">{{ $t('firefly.bills_paid') }}</span>:
|
||||||
<span v-for="(bill, index) in paid">
|
<span v-for="(bill, index) in paid"><span v-if="bill.native">(</span>{{
|
||||||
{{ formatAmount(bill.code, bill.sum) }}
|
formatAmount(bill.code, bill.sum)
|
||||||
<span v-if="index+1 !== paid.length">, </span>
|
}}<span v-if="bill.native">)</span><span
|
||||||
</span>
|
v-if="index+1 !== paid.length">, </span></span>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<!--
|
|
||||||
<q-card-section class="q-pt-xs">
|
|
||||||
<div class="text-overline">
|
|
||||||
|
|
||||||
<span class="float-right">
|
|
||||||
<span class="text-grey-4 fas fa-redo-alt" style="cursor: pointer;" @click="triggerForcedUpgrade"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section class="q-pt-xs">
|
|
||||||
<span v-for="(bill, index) in unpaid">
|
|
||||||
{{ formatAmount(bill.code, bill.sum) }}
|
|
||||||
<span v-if="index+1 !== unpaid.length">, </span>
|
|
||||||
</span>
|
|
||||||
</q-card-section>
|
|
||||||
-->
|
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -85,7 +71,7 @@ export default {
|
|||||||
store: null,
|
store: null,
|
||||||
unpaid: [],
|
unpaid: [],
|
||||||
paid: [],
|
paid: [],
|
||||||
//percentage: 0,
|
currency: 'EUR',
|
||||||
unpaidAmount: 0.0,
|
unpaidAmount: 0.0,
|
||||||
paidAmount: 0.0,
|
paidAmount: 0.0,
|
||||||
range: {
|
range: {
|
||||||
@@ -100,16 +86,18 @@ export default {
|
|||||||
if (0 === this.unpaidAmount) {
|
if (0 === this.unpaidAmount) {
|
||||||
return 100;
|
return 100;
|
||||||
}
|
}
|
||||||
const sum = this.unpaidAmount + this.paidAmount;
|
|
||||||
if (0.0 === this.paidAmount) {
|
if (0.0 === this.paidAmount) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return (this.paidAmount / sum) * 100;
|
const pct = (this.paidAmount / this.unpaidAmount) * 100;
|
||||||
|
if (pct > 100) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
return pct;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.store = useFireflyIIIStore();
|
this.store = useFireflyIIIStore();
|
||||||
|
|
||||||
// TODO this code snippet is recycled a lot.
|
// TODO this code snippet is recycled a lot.
|
||||||
if (null === this.range.start || null === this.range.end) {
|
if (null === this.range.start || null === this.range.end) {
|
||||||
// subscribe, then update:
|
// subscribe, then update:
|
||||||
@@ -133,25 +121,30 @@ export default {
|
|||||||
const start = new Date(this.store.getRange.start);
|
const start = new Date(this.store.getRange.start);
|
||||||
const end = new Date(this.store.getRange.end);
|
const end = new Date(this.store.getRange.end);
|
||||||
const sum = new Sum;
|
const sum = new Sum;
|
||||||
|
this.currency = this.store.getCurrencyCode;
|
||||||
sum.unpaid(start, end).then((response) => this.parseUnpaidResponse(response.data));
|
sum.unpaid(start, end).then((response) => this.parseUnpaidResponse(response.data));
|
||||||
sum.paid(start, end).then((response) => this.parsePaidResponse(response.data));
|
sum.paid(start, end).then((response) => this.parsePaidResponse(response.data));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// TODO this method is recycled a lot.
|
||||||
formatAmount: function (currencyCode, amount) {
|
formatAmount: function (currencyCode, amount) {
|
||||||
// TODO not yet internationalized
|
return Intl.NumberFormat(this.store.getLocale, {style: 'currency', currency: currencyCode}).format(amount);
|
||||||
return Intl.NumberFormat('en-US', {style: 'currency', currency: currencyCode}).format(amount);
|
|
||||||
},
|
},
|
||||||
parseUnpaidResponse: function (data) {
|
parseUnpaidResponse: function (data) {
|
||||||
for (let i in data) {
|
for (let i in data) {
|
||||||
if (data.hasOwnProperty(i)) {
|
if (data.hasOwnProperty(i)) {
|
||||||
const current = data[i];
|
const current = data[i];
|
||||||
|
const hasNative = current.native_id !== current.id && current.native_sum !== '0';
|
||||||
this.unpaid.push(
|
this.unpaid.push(
|
||||||
{
|
{
|
||||||
sum: current.sum,
|
sum: current.sum,
|
||||||
code: current.code,
|
code: current.code,
|
||||||
|
native: hasNative,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.unpaidAmount = this.unpaidAmount + parseFloat(current.sum);
|
if (hasNative || current.native_id === current.id) {
|
||||||
|
this.unpaidAmount = this.unpaidAmount + parseFloat(current.native_sum);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -159,20 +152,20 @@ export default {
|
|||||||
for (let i in data) {
|
for (let i in data) {
|
||||||
if (data.hasOwnProperty(i)) {
|
if (data.hasOwnProperty(i)) {
|
||||||
const current = data[i];
|
const current = data[i];
|
||||||
|
const hasNative = current.native_id !== current.id && parseFloat(current.native_sum) !== 0.0;
|
||||||
this.paid.push(
|
this.paid.push(
|
||||||
{
|
{
|
||||||
sum: current.sum,
|
sum: current.sum,
|
||||||
code: current.code,
|
code: current.code,
|
||||||
|
native: hasNative,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.paidAmount = this.paidAmount + (parseFloat(current.sum) * -1);
|
if (hasNative || current.native_id === current.id) {
|
||||||
|
this.paidAmount = this.paidAmount + (parseFloat(current.native_sum) * -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
152
frontend/src/components/dashboard/SpendInsightBox.vue
Normal file
152
frontend/src/components/dashboard/SpendInsightBox.vue
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<!--
|
||||||
|
- BillInsightBox.vue
|
||||||
|
- Copyright (c) 2022 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/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- TODO most left? q-mr-sm -->
|
||||||
|
<!-- TODO middle? dan q-mx-sm -->
|
||||||
|
<!-- TODO right? dan q-ml-sm -->
|
||||||
|
<div class="q-mx-sm">
|
||||||
|
<q-card bordered>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label><strong>Spend</strong></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator/>
|
||||||
|
<q-card-section horizontal>
|
||||||
|
<q-card-section>
|
||||||
|
<q-circular-progress
|
||||||
|
:value="percentage"
|
||||||
|
size="50px"
|
||||||
|
:thickness="0.22"
|
||||||
|
color="green"
|
||||||
|
track-color="grey-3"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator vertical/>
|
||||||
|
<q-card-section v-if="0 === budgeted.length && 0 === spent.length">
|
||||||
|
You have no budgets set
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section v-if="budgeted.length > 0 || spent.length > 0">
|
||||||
|
<span :title="formatAmount(this.currency, this.budgetedAmount)">Budgeted</span>:
|
||||||
|
<span v-for="(budget, index) in budgeted"><span v-if="budget.native">(</span>{{ formatAmount(budget.code, budget.sum) }}<span v-if="budget.native">)</span><span
|
||||||
|
v-if="index+1 !== budgeted.length">, </span></span>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {useFireflyIIIStore} from "../../stores/fireflyiii";
|
||||||
|
import Sum from "../../api/v2/budgets/sum";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
store: null,
|
||||||
|
budgeted: [],
|
||||||
|
spent: [],
|
||||||
|
currency: 'EUR',
|
||||||
|
//percentage: 0,
|
||||||
|
budgetedAmount: 0.0,
|
||||||
|
spentAmount: 0.0,
|
||||||
|
range: {
|
||||||
|
start: null,
|
||||||
|
end: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: "SpendInsightBox",
|
||||||
|
computed: {
|
||||||
|
percentage: function () {
|
||||||
|
if (0 === this.budgetedAmount) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
if (0.0 === this.spentAmount) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const pct = (this.spentAmount / this.budgetedAmount) * 100;
|
||||||
|
if (pct > 100) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
return pct;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.store = useFireflyIIIStore();
|
||||||
|
|
||||||
|
// TODO this code snippet is recycled a lot.
|
||||||
|
if (null === this.range.start || null === this.range.end) {
|
||||||
|
// subscribe, then update:
|
||||||
|
this.store.$onAction(
|
||||||
|
({name, $store, args, after, onError,}) => {
|
||||||
|
after((result) => {
|
||||||
|
if (name === 'setRange') {
|
||||||
|
this.range = result;
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.triggerUpdate();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
triggerUpdate: function () {
|
||||||
|
if (null !== this.store.getRange.start && null !== this.store.getRange.end) {
|
||||||
|
this.budgeted = [];
|
||||||
|
const start = new Date(this.store.getRange.start);
|
||||||
|
const end = new Date(this.store.getRange.end);
|
||||||
|
const sum = new Sum;
|
||||||
|
this.currency = this.store.getCurrencyCode;
|
||||||
|
sum.budgeted(start, end).then((response) => this.parseBudgetedResponse(response.data));
|
||||||
|
//sum.paid(start, end).then((response) => this.parsePaidResponse(response.data));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// TODO this method is recycled a lot.
|
||||||
|
formatAmount: function (currencyCode, amount) {
|
||||||
|
return Intl.NumberFormat(this.store.getLocale, {style: 'currency', currency: currencyCode}).format(amount);
|
||||||
|
},
|
||||||
|
parseBudgetedResponse: function (data) {
|
||||||
|
for (let i in data) {
|
||||||
|
if (data.hasOwnProperty(i)) {
|
||||||
|
const current = data[i];
|
||||||
|
const hasNative = current.native_id !== current.id && parseFloat(current.native_sum) !== 0.0;
|
||||||
|
this.budgeted.push(
|
||||||
|
{
|
||||||
|
sum: current.sum,
|
||||||
|
code: current.code,
|
||||||
|
native: hasNative
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (hasNative || current.native_id === current.id) {
|
||||||
|
this.budgetedAmount = this.budgetedAmount + parseFloat(current.native_sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
@@ -27,7 +27,7 @@
|
|||||||
<BillInsightBox />
|
<BillInsightBox />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
TODO spend insight
|
<SpendInsightBox />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
TODO net worth insight
|
TODO net worth insight
|
||||||
@@ -94,6 +94,7 @@ export default {
|
|||||||
name: "Dashboard",
|
name: "Dashboard",
|
||||||
components: {
|
components: {
|
||||||
BillInsightBox: defineAsyncComponent(() => import('../../components/dashboard/BillInsightBox.vue')),
|
BillInsightBox: defineAsyncComponent(() => import('../../components/dashboard/BillInsightBox.vue')),
|
||||||
|
SpendInsightBox: defineAsyncComponent(() => import('../../components/dashboard/SpendInsightBox.vue')),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
94
frontend/src/store/fireflyiii/actions.js
vendored
94
frontend/src/store/fireflyiii/actions.js
vendored
@@ -36,97 +36,3 @@ export function resetRange(context) {
|
|||||||
context.commit('setRange', defaultRange);
|
context.commit('setRange', defaultRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setDatesFromViewRange(context) {
|
|
||||||
let start;
|
|
||||||
let end;
|
|
||||||
let viewRange = context.getters.getViewRange;
|
|
||||||
|
|
||||||
let today = new Date;
|
|
||||||
switch (viewRange) {
|
|
||||||
case 'last365':
|
|
||||||
start = startOfDay(subDays(today, 365));
|
|
||||||
end = endOfDay(today);
|
|
||||||
break;
|
|
||||||
case 'last90':
|
|
||||||
start = startOfDay(subDays(today, 90));
|
|
||||||
end = endOfDay(today);
|
|
||||||
break;
|
|
||||||
case 'last30':
|
|
||||||
start = startOfDay(subDays(today, 30));
|
|
||||||
end = endOfDay(today);
|
|
||||||
break;
|
|
||||||
case 'last7':
|
|
||||||
start = startOfDay(subDays(today, 7));
|
|
||||||
end = endOfDay(today);
|
|
||||||
break;
|
|
||||||
case 'YTD':
|
|
||||||
start = startOfYear(today);
|
|
||||||
end = endOfDay(today);
|
|
||||||
break;
|
|
||||||
case 'QTD':
|
|
||||||
start = startOfQuarter(today);
|
|
||||||
end = endOfDay(today);
|
|
||||||
break;
|
|
||||||
case 'MTD':
|
|
||||||
start = startOfMonth(today);
|
|
||||||
end = endOfDay(today);
|
|
||||||
break;
|
|
||||||
case '1D':
|
|
||||||
// today:
|
|
||||||
start = startOfDay(today);
|
|
||||||
end = endOfDay(today);
|
|
||||||
break;
|
|
||||||
case '1W':
|
|
||||||
// this week:
|
|
||||||
start = startOfDay(startOfWeek(today, {weekStartsOn: 1}));
|
|
||||||
end = endOfDay(endOfWeek(today, {weekStartsOn: 1}));
|
|
||||||
break;
|
|
||||||
case '1M':
|
|
||||||
// this month:
|
|
||||||
start = startOfDay(startOfMonth(today));
|
|
||||||
end = endOfDay(endOfMonth(today));
|
|
||||||
break;
|
|
||||||
case '3M':
|
|
||||||
// this quarter
|
|
||||||
start = startOfDay(startOfQuarter(today));
|
|
||||||
end = endOfDay(endOfQuarter(today));
|
|
||||||
break;
|
|
||||||
case '6M':
|
|
||||||
// this half-year
|
|
||||||
if (today.getMonth() <= 5) {
|
|
||||||
start = new Date(today);
|
|
||||||
start.setMonth(0);
|
|
||||||
start.setDate(1);
|
|
||||||
start = startOfDay(start);
|
|
||||||
end = new Date(today);
|
|
||||||
end.setMonth(5);
|
|
||||||
end.setDate(30);
|
|
||||||
end = endOfDay(start);
|
|
||||||
}
|
|
||||||
if (today.getMonth() > 5) {
|
|
||||||
start = new Date(today);
|
|
||||||
start.setMonth(6);
|
|
||||||
start.setDate(1);
|
|
||||||
start = startOfDay(start);
|
|
||||||
end = new Date(today);
|
|
||||||
end.setMonth(11);
|
|
||||||
end.setDate(31);
|
|
||||||
end = endOfDay(start);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case '1Y':
|
|
||||||
// this year
|
|
||||||
start = new Date(today);
|
|
||||||
start.setMonth(0);
|
|
||||||
start.setDate(1);
|
|
||||||
start = startOfDay(start);
|
|
||||||
|
|
||||||
end = new Date(today);
|
|
||||||
end.setMonth(11);
|
|
||||||
end.setDate(31);
|
|
||||||
end = endOfDay(end);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
context.commit('setRange', {start: start, end: end});
|
|
||||||
context.commit('setDefaultRange', {start: start, end: end});
|
|
||||||
}
|
|
||||||
|
28
frontend/src/stores/fireflyiii.js
vendored
28
frontend/src/stores/fireflyiii.js
vendored
@@ -12,19 +12,31 @@ import {
|
|||||||
subDays
|
subDays
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
|
|
||||||
export const useFireflyIIIStore = defineStore('counter', {
|
export const useFireflyIIIStore = defineStore('firefly-iii', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
drawerState: true, viewRange: '1M', listPageSize: 10, range: {
|
drawerState: true,
|
||||||
|
viewRange: '1M',
|
||||||
|
listPageSize: 10,
|
||||||
|
locale: 'en-US',
|
||||||
|
range: {
|
||||||
start: null, end: null
|
start: null, end: null
|
||||||
}, defaultRange: {
|
},
|
||||||
start: null, end: null
|
defaultRange: {
|
||||||
}, currencyCode: 'AAA', currencyId: '0', cacheKey: 'initial'
|
start: null,
|
||||||
|
end: null
|
||||||
|
},
|
||||||
|
currencyCode: 'AAA',
|
||||||
|
currencyId: '0',
|
||||||
|
cacheKey: 'initial'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
getViewRange(state) {
|
getViewRange(state) {
|
||||||
return state.viewRange;
|
return state.viewRange;
|
||||||
},
|
},
|
||||||
|
getLocale(state) {
|
||||||
|
return state.locale;
|
||||||
|
},
|
||||||
|
|
||||||
getListPageSize(state) {
|
getListPageSize(state) {
|
||||||
return state.listPageSize;
|
return state.listPageSize;
|
||||||
@@ -156,9 +168,6 @@ export const useFireflyIIIStore = defineStore('counter', {
|
|||||||
|
|
||||||
// mutators
|
// mutators
|
||||||
|
|
||||||
increment() {
|
|
||||||
this.counter++
|
|
||||||
},
|
|
||||||
updateViewRange(viewRange) {
|
updateViewRange(viewRange) {
|
||||||
this.viewRange = viewRange;
|
this.viewRange = viewRange;
|
||||||
},
|
},
|
||||||
@@ -166,6 +175,9 @@ export const useFireflyIIIStore = defineStore('counter', {
|
|||||||
updateListPageSize(value) {
|
updateListPageSize(value) {
|
||||||
this.listPageSize = value;
|
this.listPageSize = value;
|
||||||
},
|
},
|
||||||
|
setLocale(value) {
|
||||||
|
this.locale = value;
|
||||||
|
},
|
||||||
|
|
||||||
setRange(value) {
|
setRange(value) {
|
||||||
this.range = value;
|
this.range = value;
|
||||||
|
@@ -67,11 +67,11 @@
|
|||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
|
|
||||||
{% if config('firefly.authentication_guard') == 'web' %}
|
{% if config('firefly.authentication_guard') == 'web' %}
|
||||||
<input type="email" class="form-control" name="email"
|
<input type="email" id="focus" class="form-control" name="email"
|
||||||
placeholder="{{ trans('form.email') }}"
|
placeholder="{{ trans('form.email') }}"
|
||||||
value="{% if not IS_DEMO_SITE %}{{ email }}{% else %}{{ DEMO_USERNAME }}{% endif %}">
|
value="{% if not IS_DEMO_SITE %}{{ email }}{% else %}{{ DEMO_USERNAME }}{% endif %}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<input type="text" autocomplete="username" name="{{ usernameField }}" value="{{ email }}"
|
<input type="text" id="focus" autocomplete="username" name="{{ usernameField }}" value="{{ email }}"
|
||||||
class="form-control" placeholder="{{ trans('form.login_name') }}"/>
|
class="form-control" placeholder="{{ trans('form.login_name') }}"/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@@ -126,4 +126,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block scripts %}
|
||||||
|
<script nonce="{{ JS_NONCE }}">
|
||||||
|
$(function () {
|
||||||
|
"use strict";
|
||||||
|
$('#focus').focus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
@@ -7,19 +7,21 @@
|
|||||||
<title>{{ 'login_page_title'|_ }}</title>
|
<title>{{ 'login_page_title'|_ }}</title>
|
||||||
|
|
||||||
<!-- fonts and styles -->
|
<!-- fonts and styles -->
|
||||||
<link rel="stylesheet" href="/v3-local/css/fonts.css">
|
<link rel="stylesheet" href="/v3-local/css/fonts.css?v={{ FF_VERSION }}">
|
||||||
<link rel="stylesheet" href="/v3-local/lib/fontawesome-free/css/all.min.css">
|
<link rel="stylesheet" href="/v3-local/lib/fontawesome-free/css/all.min.css?v={{ FF_VERSION }}">
|
||||||
<link rel="stylesheet" href="/v3-local/lib/icheck-bootstrap/icheck-bootstrap.min.css">
|
<link rel="stylesheet" href="/v3-local/lib/icheck-bootstrap/icheck-bootstrap.min.css?v={{ FF_VERSION }}">
|
||||||
<link rel="stylesheet" href="/v3-local/dist/css/adminlte.min.css">
|
<link rel="stylesheet" href="/v3-local/dist/css/adminlte.min.css?v={{ FF_VERSION }}">
|
||||||
</head>
|
</head>
|
||||||
<body class="hold-transition login-page dark-mode">
|
<body class="hold-transition login-page dark-mode">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
<!-- jQuery -->
|
<!-- jQuery -->
|
||||||
<script src="v3-local/lib/jquery/jquery.min.js"></script>
|
<script src="v3-local/lib/jquery/jquery.min.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script>
|
||||||
<!-- Bootstrap 4 -->
|
<!-- Bootstrap 4 -->
|
||||||
<script src="v3-local/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
|
<script src="v3-local/lib/bootstrap/js/bootstrap.bundle.min.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script>
|
||||||
<!-- AdminLTE App -->
|
<!-- AdminLTE App -->
|
||||||
<script src="v3-local/dist/js/adminlte.min.js"></script>
|
<script src="v3-local/dist/js/adminlte.min.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script>
|
||||||
|
{% block scripts %}
|
||||||
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -45,6 +45,29 @@ Route::group(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API route for bills.
|
||||||
|
*/
|
||||||
|
Route::group(
|
||||||
|
['namespace' => 'FireflyIII\Api\V2\Controllers\Model\Budget', 'prefix' => 'v2/budgets',
|
||||||
|
'as' => 'api.v2.budgets',],
|
||||||
|
static function () {
|
||||||
|
Route::get('sum/budgeted', ['uses' => 'SumController@budgeted', 'as' => 'sum.budgeted']);
|
||||||
|
Route::get('sum/unpaid', ['uses' => 'SumController@unpaid', 'as' => 'sum.unpaid']);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API route for system
|
||||||
|
*/
|
||||||
|
Route::group(
|
||||||
|
['namespace' => 'FireflyIII\Api\V2\Controllers\System', 'prefix' => 'v2',
|
||||||
|
'as' => 'api.v2.system.',],
|
||||||
|
static function () {
|
||||||
|
Route::get('preferences/{preference}', ['uses' => 'PreferencesController@get', 'as' => 'preferences.get']);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Autocomplete controllers
|
* Autocomplete controllers
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user