mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-02-25 18:45:27 -06:00
Various code for currency exchange rate support
This commit is contained in:
parent
9c08b9f1d3
commit
d007db166a
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;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Database\Seeders\ExchangeRateSeeder;
|
||||
use Exception;
|
||||
use FireflyIII\Events\ActuallyLoggedIn;
|
||||
use FireflyIII\Events\DetectedNewIPAddress;
|
||||
@ -75,6 +76,17 @@ class UserEventHandler
|
||||
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.
|
||||
*
|
||||
|
@ -24,47 +24,17 @@ namespace FireflyIII\Models;
|
||||
|
||||
use Eloquent;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* FireflyIII\Models\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
|
||||
*/
|
||||
class CurrencyExchangeRate extends Model
|
||||
{
|
||||
/** @var array Convert these fields to other data types */
|
||||
protected $casts
|
||||
= [
|
||||
= [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'user_id' => 'int',
|
||||
@ -72,6 +42,7 @@ class CurrencyExchangeRate extends Model
|
||||
'to_currency_id' => 'int',
|
||||
'date' => 'datetime',
|
||||
];
|
||||
protected $fillable = ['user_id', 'from_currency_id', 'to_currency_id', 'date', 'rate'];
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
|
@ -83,6 +83,9 @@ class Preference extends Model
|
||||
$user = auth()->user();
|
||||
/** @var Preference|null $preference */
|
||||
$preference = $user->preferences()->where('name', $value)->first();
|
||||
if (null === $preference) {
|
||||
$preference = $user->preferences()->where('id', (int) $value)->first();
|
||||
}
|
||||
if (null !== $preference) {
|
||||
return $preference;
|
||||
}
|
||||
|
@ -69,6 +69,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail',
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole',
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@createGroupMembership',
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@createExchangeRates',
|
||||
],
|
||||
// is a User related event.
|
||||
Login::class => [
|
||||
|
@ -33,6 +33,7 @@ use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use JsonException;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
@ -304,7 +305,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface
|
||||
*
|
||||
* @return BudgetLimit
|
||||
* @throws FireflyException
|
||||
* @throws \JsonException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function store(array $data): BudgetLimit
|
||||
{
|
||||
@ -354,7 +355,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface
|
||||
*
|
||||
* @return BudgetLimit
|
||||
* @throws FireflyException
|
||||
* @throws \JsonException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function update(BudgetLimit $budgetLimit, array $data): BudgetLimit
|
||||
{
|
||||
|
@ -39,6 +39,7 @@ use FireflyIII\Services\Internal\Destroy\BudgetDestroyService;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Collection;
|
||||
use JsonException;
|
||||
use Log;
|
||||
use Storage;
|
||||
|
||||
@ -80,6 +81,60 @@ class BudgetRepository implements BudgetRepositoryInterface
|
||||
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
|
||||
*/
|
||||
@ -335,7 +390,7 @@ class BudgetRepository implements BudgetRepositoryInterface
|
||||
*
|
||||
* @return Budget
|
||||
* @throws FireflyException
|
||||
* @throws \JsonException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function store(array $data): Budget
|
||||
{
|
||||
@ -551,7 +606,7 @@ class BudgetRepository implements BudgetRepositoryInterface
|
||||
* @param Budget $budget
|
||||
* @param array $data
|
||||
* @throws FireflyException
|
||||
* @throws \JsonException
|
||||
* @throws JsonException
|
||||
*/
|
||||
private function updateAutoBudget(Budget $budget, array $data): void
|
||||
{
|
||||
@ -597,4 +652,42 @@ class BudgetRepository implements BudgetRepositoryInterface
|
||||
|
||||
$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
|
||||
{
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param int $limit
|
||||
@ -50,6 +51,15 @@ interface BudgetRepositoryInterface
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
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(ConfigSeeder::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>
|
||||
import {defineComponent} from 'vue';
|
||||
import Preferences from "./api/preferences";
|
||||
import Prefs from "./api/v2/preferences";
|
||||
import Currencies from "./api/currencies";
|
||||
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(() => {
|
||||
getViewRange();
|
||||
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-item>
|
||||
<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>
|
||||
<q-separator/>
|
||||
@ -41,36 +41,22 @@
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-separator vertical/>
|
||||
<q-card-section>
|
||||
{{ $t('firefly.bills_to_pay') }}:
|
||||
<q-card-section v-if="0 === unpaid.length && 0 === paid.length">
|
||||
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">
|
||||
{{ formatAmount(bill.code, bill.sum) }}
|
||||
<span v-if="index+1 !== unpaid.length">, </span>
|
||||
</span>
|
||||
<span v-if="bill.native">(</span>{{ formatAmount(bill.code, bill.sum) }}<span
|
||||
v-if="bill.native">)</span><span v-if="index+1 !== unpaid.length">, </span></span>
|
||||
<br/>
|
||||
{{ $t('firefly.bills_paid') }}:
|
||||
<span v-for="(bill, index) in paid">
|
||||
{{ formatAmount(bill.code, bill.sum) }}
|
||||
<span v-if="index+1 !== paid.length">, </span>
|
||||
</span>
|
||||
<span :title="formatAmount(this.currency, this.paidAmount)">{{ $t('firefly.bills_paid') }}</span>:
|
||||
<span v-for="(bill, index) in paid"><span v-if="bill.native">(</span>{{
|
||||
formatAmount(bill.code, bill.sum)
|
||||
}}<span v-if="bill.native">)</span><span
|
||||
v-if="index+1 !== paid.length">, </span></span>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
@ -85,7 +71,7 @@ export default {
|
||||
store: null,
|
||||
unpaid: [],
|
||||
paid: [],
|
||||
//percentage: 0,
|
||||
currency: 'EUR',
|
||||
unpaidAmount: 0.0,
|
||||
paidAmount: 0.0,
|
||||
range: {
|
||||
@ -100,16 +86,18 @@ export default {
|
||||
if (0 === this.unpaidAmount) {
|
||||
return 100;
|
||||
}
|
||||
const sum = this.unpaidAmount + this.paidAmount;
|
||||
if (0.0 === this.paidAmount) {
|
||||
return 0;
|
||||
}
|
||||
return (this.paidAmount / sum) * 100;
|
||||
const pct = (this.paidAmount / this.unpaidAmount) * 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:
|
||||
@ -133,25 +121,30 @@ export default {
|
||||
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.unpaid(start, end).then((response) => this.parseUnpaidResponse(response.data));
|
||||
sum.paid(start, end).then((response) => this.parsePaidResponse(response.data));
|
||||
}
|
||||
},
|
||||
// TODO this method is recycled a lot.
|
||||
formatAmount: function (currencyCode, amount) {
|
||||
// TODO not yet internationalized
|
||||
return Intl.NumberFormat('en-US', {style: 'currency', currency: currencyCode}).format(amount);
|
||||
return Intl.NumberFormat(this.store.getLocale, {style: 'currency', currency: currencyCode}).format(amount);
|
||||
},
|
||||
parseUnpaidResponse: function (data) {
|
||||
for (let i in data) {
|
||||
if (data.hasOwnProperty(i)) {
|
||||
const current = data[i];
|
||||
const hasNative = current.native_id !== current.id && current.native_sum !== '0';
|
||||
this.unpaid.push(
|
||||
{
|
||||
sum: current.sum,
|
||||
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) {
|
||||
if (data.hasOwnProperty(i)) {
|
||||
const current = data[i];
|
||||
const hasNative = current.native_id !== current.id && parseFloat(current.native_sum) !== 0.0;
|
||||
this.paid.push(
|
||||
{
|
||||
sum: current.sum,
|
||||
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>
|
||||
|
||||
<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 />
|
||||
</div>
|
||||
<div class="col">
|
||||
TODO spend insight
|
||||
<SpendInsightBox />
|
||||
</div>
|
||||
<div class="col">
|
||||
TODO net worth insight
|
||||
@ -94,6 +94,7 @@ export default {
|
||||
name: "Dashboard",
|
||||
components: {
|
||||
BillInsightBox: defineAsyncComponent(() => import('../../components/dashboard/BillInsightBox.vue')),
|
||||
SpendInsightBox: defineAsyncComponent(() => import('../../components/dashboard/SpendInsightBox.vue')),
|
||||
}
|
||||
}
|
||||
</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);
|
||||
}
|
||||
|
||||
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
|
||||
} from "date-fns";
|
||||
|
||||
export const useFireflyIIIStore = defineStore('counter', {
|
||||
export const useFireflyIIIStore = defineStore('firefly-iii', {
|
||||
state: () => ({
|
||||
drawerState: true, viewRange: '1M', listPageSize: 10, range: {
|
||||
drawerState: true,
|
||||
viewRange: '1M',
|
||||
listPageSize: 10,
|
||||
locale: 'en-US',
|
||||
range: {
|
||||
start: null, end: null
|
||||
}, defaultRange: {
|
||||
start: null, end: null
|
||||
}, currencyCode: 'AAA', currencyId: '0', cacheKey: 'initial'
|
||||
},
|
||||
defaultRange: {
|
||||
start: null,
|
||||
end: null
|
||||
},
|
||||
currencyCode: 'AAA',
|
||||
currencyId: '0',
|
||||
cacheKey: 'initial'
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getViewRange(state) {
|
||||
return state.viewRange;
|
||||
},
|
||||
getLocale(state) {
|
||||
return state.locale;
|
||||
},
|
||||
|
||||
getListPageSize(state) {
|
||||
return state.listPageSize;
|
||||
@ -156,9 +168,6 @@ export const useFireflyIIIStore = defineStore('counter', {
|
||||
|
||||
// mutators
|
||||
|
||||
increment() {
|
||||
this.counter++
|
||||
},
|
||||
updateViewRange(viewRange) {
|
||||
this.viewRange = viewRange;
|
||||
},
|
||||
@ -166,6 +175,9 @@ export const useFireflyIIIStore = defineStore('counter', {
|
||||
updateListPageSize(value) {
|
||||
this.listPageSize = value;
|
||||
},
|
||||
setLocale(value) {
|
||||
this.locale = value;
|
||||
},
|
||||
|
||||
setRange(value) {
|
||||
this.range = value;
|
||||
|
@ -67,11 +67,11 @@
|
||||
<div class="input-group mb-3">
|
||||
|
||||
{% 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') }}"
|
||||
value="{% if not IS_DEMO_SITE %}{{ email }}{% else %}{{ DEMO_USERNAME }}{% endif %}">
|
||||
{% 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') }}"/>
|
||||
{% endif %}
|
||||
<div class="input-group-append">
|
||||
@ -126,4 +126,11 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script nonce="{{ JS_NONCE }}">
|
||||
$(function () {
|
||||
"use strict";
|
||||
$('#focus').focus();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -7,19 +7,21 @@
|
||||
<title>{{ 'login_page_title'|_ }}</title>
|
||||
|
||||
<!-- fonts and styles -->
|
||||
<link rel="stylesheet" href="/v3-local/css/fonts.css">
|
||||
<link rel="stylesheet" href="/v3-local/lib/fontawesome-free/css/all.min.css">
|
||||
<link rel="stylesheet" href="/v3-local/lib/icheck-bootstrap/icheck-bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/v3-local/dist/css/adminlte.min.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?v={{ FF_VERSION }}">
|
||||
<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?v={{ FF_VERSION }}">
|
||||
</head>
|
||||
<body class="hold-transition login-page dark-mode">
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
<!-- 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 -->
|
||||
<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 -->
|
||||
<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>
|
||||
</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
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user