Compare commits

...

11 Commits

Author SHA1 Message Date
github-actions[bot]
c525c70ec0 Merge pull request #10657 from firefly-iii/release-1753673814
🤖 Automatically merge the PR into the develop branch.
2025-07-28 05:37:03 +02:00
JC5
1f7d6e218b 🤖 Auto commit for release 'develop' on 2025-07-28 2025-07-28 05:36:54 +02:00
James Cole
a69b6d9ce2 Respond to "convert to native". 2025-07-27 20:45:08 +02:00
James Cole
28b2ddde18 Account chart can do live update 2025-07-27 07:36:23 +02:00
James Cole
a16cc73c77 Sync "convert to native" 2025-07-26 19:17:26 +02:00
James Cole
46395e350a Improved budget chart. 2025-07-26 06:47:21 +02:00
Sander Dorigo
f62e49090c Merge branch 'main' into develop 2025-07-25 11:18:24 +02:00
James Cole
af3b40a314 Delete .github/workflows/sonarcloud.yml
Signed-off-by: James Cole <james@firefly-iii.org>
2025-07-25 11:10:05 +02:00
James Cole
c3cea0fa9e Update cleanup.yml
Signed-off-by: James Cole <james@firefly-iii.org>
2025-07-25 11:09:55 +02:00
James Cole
6cbdb2ce70 Update index.js
Fix thnigie

Signed-off-by: James Cole <james@firefly-iii.org>
2025-07-25 10:56:42 +02:00
James Cole
b78460100d Fix #10646 2025-07-25 05:44:03 +02:00
33 changed files with 468 additions and 303 deletions

View File

@@ -66,7 +66,6 @@ jobs:
'label-actions.yml',
'lock.yml',
'release.yml',
'sonarcloud.yml',
'stale.yml'
]

View File

@@ -24,16 +24,16 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Chart;
use FireflyIII\Exceptions\ValidationException;
use FireflyIII\Models\TransactionCurrency;
use Carbon\Carbon;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Chart\ChartRequest;
use FireflyIII\Api\V1\Requests\Data\DateRequest;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Exceptions\ValidationException;
use FireflyIII\Models\Account;
use FireflyIII\Models\Preference;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Chart\ChartData;
use FireflyIII\Support\Facades\Preferences;
@@ -42,6 +42,7 @@ use FireflyIII\Support\Http\Api\ApiSupport;
use FireflyIII\Support\Http\Api\CollectsAccountsFromFilter;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
/**
* Class AccountController
@@ -86,10 +87,12 @@ class AccountController extends Controller
// move date to end of day
$queryParameters['start']->startOfDay();
$queryParameters['end']->endOfDay();
Log::debug(sprintf('dashboard(), convert to native: %s', var_export($this->convertToNative, true)));
// loop each account, and collect info:
/** @var Account $account */
foreach ($accounts as $account) {
Log::debug(sprintf('Account #%d ("%s")', $account->id, $account->name));
$this->renderAccountData($queryParameters, $account);
}
@@ -101,15 +104,22 @@ class AccountController extends Controller
*/
private function renderAccountData(array $params, Account $account): void
{
$currency = $this->repository->getAccountCurrency($account);
Log::debug(sprintf('Now in %s(array, #%d)', __METHOD__, $account->id));
$currency = $this->repository->getAccountCurrency($account);
$currentStart = clone $params['start'];
$range = Steam::finalAccountBalanceInRange($account, $params['start'], clone $params['end'], $this->convertToNative);
$previous = array_values($range)[0]['balance'];
$nativePrevious = null;
if (!$currency instanceof TransactionCurrency) {
$currency = $this->default;
}
$currentSet = [
$currentSet = [
'label' => $account->name,
// the currency that belongs to the account.
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
@@ -121,18 +131,33 @@ class AccountController extends Controller
'period' => '1D',
'entries' => [],
];
$currentStart = clone $params['start'];
$range = Steam::finalAccountBalanceInRange($account, $params['start'], clone $params['end'], $this->convertToNative);
if ($this->convertToNative) {
$currentSet['native_entries'] = [];
$currentSet['native_currency_id'] = (string)$this->nativeCurrency->id;
$currentSet['native_currency_code'] = $this->nativeCurrency->code;
$currentSet['native_currency_symbol'] = $this->nativeCurrency->symbol;
$currentSet['native_currency_decimal_places'] = $this->nativeCurrency->decimal_places;
$nativePrevious = array_values($range)[0]['native_balance'];
}
$previous = array_values($range)[0]['balance'];
while ($currentStart <= $params['end']) {
$format = $currentStart->format('Y-m-d');
$label = $currentStart->toAtomString();
$balance = array_key_exists($format, $range) ? $range[$format]['balance'] : $previous;
$previous = $balance;
$currentSet['entries'][$label] = $balance;
// do the same for the native balance, if relevant:
$nativeBalance = null;
if ($this->convertToNative) {
$nativeBalance = array_key_exists($format, $range) ? $range[$format]['native_balance'] : $nativePrevious;
$nativePrevious = $nativeBalance;
$currentSet['native_entries'][$label] = $nativeBalance;
}
$currentStart->addDay();
$currentSet['entries'][$label] = $balance;
}
$this->chartData->add($currentSet);
}
@@ -146,19 +171,84 @@ class AccountController extends Controller
public function overview(DateRequest $request): JsonResponse
{
// parameters for chart:
$dates = $request->getAll();
$dates = $request->getAll();
/** @var Carbon $start */
$start = $dates['start'];
$start = $dates['start'];
/** @var Carbon $end */
$end = $dates['end'];
$end = $dates['end'];
// set dates to end of day + start of day:
$start->startOfDay();
$end->endOfDay();
// user's preferences
$frontPageIds = $this->getFrontPageAccountIds();
$accounts = $this->repository->getAccountsById($frontPageIds);
$chartData = [];
/** @var Account $account */
foreach ($accounts as $account) {
Log::debug(sprintf('Rendering chart data for account %s (%d)', $account->name, $account->id));
$currency = $this->repository->getAccountCurrency($account) ?? $this->nativeCurrency;
$currentStart = clone $start;
$range = Steam::finalAccountBalanceInRange($account, $start, clone $end, $this->convertToNative);
$previous = array_values($range)[0]['balance'];
$nativePrevious = null;
$currentSet = [
'label' => $account->name,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'start_date' => $start->toAtomString(),
'end_date' => $end->toAtomString(),
'type' => 'line', // line, area or bar
'yAxisID' => 0, // 0, 1, 2
'entries' => [],
];
// add "native_entries" if convertToNative is true:
if ($this->convertToNative) {
$currentSet['native_entries'] = [];
$currentSet['native_currency_id'] = (string)$this->nativeCurrency->id;
$currentSet['native_currency_code'] = $this->nativeCurrency->code;
$currentSet['native_currency_symbol'] = $this->nativeCurrency->symbol;
$currentSet['native_currency_decimal_places'] = $this->nativeCurrency->decimal_places;
$nativePrevious = array_values($range)[0]['native_balance'];
}
// also get the native balance if convertToNative is true:
while ($currentStart <= $end) {
$format = $currentStart->format('Y-m-d');
$label = $currentStart->toAtomString();
// balance is based on "balance" from the $range variable.
$balance = array_key_exists($format, $range) ? $range[$format]['balance'] : $previous;
$previous = $balance;
$currentSet['entries'][$label] = $balance;
// do the same for the native balance, if relevant:
$nativeBalance = null;
if ($this->convertToNative) {
$nativeBalance = array_key_exists($format, $range) ? $range[$format]['native_balance'] : $nativePrevious;
$nativePrevious = $nativeBalance;
$currentSet['native_entries'][$label] = $nativeBalance;
}
$currentStart->addDay();
}
$chartData[] = $currentSet;
}
return response()->json($chartData);
}
private function getFrontPageAccountIds(): array
{
$defaultSet = $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value])->pluck('id')->toArray();
/** @var Preference $frontpage */
@@ -169,41 +259,6 @@ class AccountController extends Controller
$frontpage->save();
}
// get accounts:
$accounts = $this->repository->getAccountsById($frontpage->data);
$chartData = [];
/** @var Account $account */
foreach ($accounts as $account) {
$currency = $this->repository->getAccountCurrency($account) ?? $this->nativeCurrency;
$field = $this->convertToNative && $currency->id !== $this->nativeCurrency->id ? 'native_balance' : 'balance';
$currentSet = [
'label' => $account->name,
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'start_date' => $start->toAtomString(),
'end_date' => $end->toAtomString(),
'type' => 'line', // line, area or bar
'yAxisID' => 0, // 0, 1, 2
'entries' => [],
];
// TODO this code is also present in the V2 chart account controller so this method is due to be deprecated.
$currentStart = clone $start;
$range = Steam::finalAccountBalanceInRange($account, $start, clone $end, $this->convertToNative);
$previous = array_values($range)[0][$field];
while ($currentStart <= $end) {
$format = $currentStart->format('Y-m-d');
$label = $currentStart->toAtomString();
$balance = array_key_exists($format, $range) ? $range[$format][$field] : $previous;
$previous = $balance;
$currentStart->addDay();
$currentSet['entries'][$label] = $balance;
}
$chartData[] = $currentSet;
}
return response()->json($chartData);
return $frontpage->data ?? $defaultSet;
}
}

View File

@@ -123,7 +123,7 @@ class BudgetController extends Controller
foreach ($rows as $row) {
$current = [
'label' => $budget->name,
'currency_id' => (string) $row['currency_id'],
'currency_id' => (string)$row['currency_id'],
'currency_code' => $row['currency_code'],
'currency_name' => $row['currency_name'],
'currency_decimal_places' => $row['currency_decimal_places'],
@@ -131,6 +131,7 @@ class BudgetController extends Controller
'start' => $row['start'],
'end' => $row['end'],
'entries' => [
'budgeted' => $row['budgeted'],
'spent' => $row['spent'],
'left' => $row['left'],
'overspent' => $row['overspent'],
@@ -159,11 +160,9 @@ class BudgetController extends Controller
* Shared between the "noBudgetLimits" function and "processLimit". Will take a single set of expenses and return
* its info.
*
* @param array<int, array<int, string>> $array
*
* @throws FireflyException
*/
private function processExpenses(int $budgetId, array $array, Carbon $start, Carbon $end): array
private function processExpenses(int $budgetId, array $spent, Carbon $start, Carbon $end): array
{
$return = [];
@@ -174,16 +173,17 @@ class BudgetController extends Controller
* @var int $currencyId
* @var array $block
*/
foreach ($array as $currencyId => $block) {
foreach ($spent as $currencyId => $block) {
$this->currencies[$currencyId] ??= TransactionCurrency::find($currencyId);
$return[$currencyId] ??= [
'currency_id' => (string) $currencyId,
'currency_id' => (string)$currencyId,
'currency_code' => $block['currency_code'],
'currency_name' => $block['currency_name'],
'currency_symbol' => $block['currency_symbol'],
'currency_decimal_places' => (int) $block['currency_decimal_places'],
'currency_decimal_places' => (int)$block['currency_decimal_places'],
'start' => $start->toAtomString(),
'end' => $end->toAtomString(),
'budgeted' => '0',
'spent' => '0',
'left' => '0',
'overspent' => '0',
@@ -193,7 +193,7 @@ class BudgetController extends Controller
// var_dump($return);
/** @var array $journal */
foreach ($currentBudgetArray['transaction_journals'] as $journal) {
$return[$currencyId]['spent'] = bcadd($return[$currencyId]['spent'], (string) $journal['amount']);
$return[$currencyId]['spent'] = bcadd($return[$currencyId]['spent'], (string)$journal['amount']);
}
}
@@ -240,13 +240,14 @@ class BudgetController extends Controller
$filtered = array_filter($spent, fn ($entry) => $entry['currency_id'] === $limitCurrencyId);
$result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end);
if (1 === count($result)) {
$compare = bccomp($limit->amount, (string) app('steam')->positive($result[$limitCurrencyId]['spent']));
$compare = bccomp($limit->amount, (string)app('steam')->positive($result[$limitCurrencyId]['spent']));
$result[$limitCurrencyId]['budgeted'] = $limit->amount;
if (1 === $compare) {
// convert this amount into the native currency:
$result[$limitCurrencyId]['left'] = bcadd($limit->amount, (string) $result[$limitCurrencyId]['spent']);
$result[$limitCurrencyId]['left'] = bcadd($limit->amount, (string)$result[$limitCurrencyId]['spent']);
}
if ($compare <= 0) {
$result[$limitCurrencyId]['overspent'] = app('steam')->positive(bcadd($limit->amount, (string) $result[$limitCurrencyId]['spent']));
$result[$limitCurrencyId]['overspent'] = app('steam')->positive(bcadd($limit->amount, (string)$result[$limitCurrencyId]['spent']));
}
}

View File

@@ -147,6 +147,7 @@ class ShowController extends Controller
$enrichment->setUser($admin);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters($this->parameters);

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Handlers\Events;
use FireflyIII\Jobs\SendWebhookMessage;
use FireflyIII\Models\WebhookMessage;
use Illuminate\Support\Facades\Log;
/**
* Class WebhookEventHandler
@@ -37,7 +38,13 @@ class WebhookEventHandler
*/
public function sendWebhookMessages(): void
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
Log::debug(sprintf('Now in %s', __METHOD__));
if (false === config('firefly.feature_flags.webhooks') || false === config('firefly.allow_webhooks')) {
Log::info('Webhook event handler is disabled, do not run sendWebhookMessages().');
return;
}
// kick off the job!
$messages = WebhookMessage::where('webhook_messages.sent', false)
->get(['webhook_messages.*'])
@@ -45,14 +52,14 @@ class WebhookEventHandler
static fn (WebhookMessage $message) => $message->webhookAttempts()->count() <= 2
)->splice(0, 5)
;
app('log')->debug(sprintf('Found %d webhook message(s) ready to be send.', $messages->count()));
Log::debug(sprintf('Found %d webhook message(s) ready to be send.', $messages->count()));
foreach ($messages as $message) {
if (false === $message->sent) {
app('log')->debug(sprintf('Send message #%d', $message->id));
Log::debug(sprintf('Send message #%d', $message->id));
SendWebhookMessage::dispatch($message)->afterResponse();
}
if (false !== $message->sent) {
app('log')->debug(sprintf('Skip message #%d', $message->id));
Log::debug(sprintf('Skip message #%d', $message->id));
}
}
}

View File

@@ -558,7 +558,6 @@ class GroupCollector implements GroupCollectorInterface
$groups[$groupId]['transactions'][$journalId] = $this->parseAugmentedJournal($augumentedJournal);
}
}
$groups = $this->parseSums($groups);
return new Collection($groups);

View File

@@ -32,6 +32,7 @@ use FireflyIII\Models\UserGroup;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use NumberFormatter;
/**
@@ -66,13 +67,13 @@ class Amount
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $symbol);
$fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimalPlaces);
$fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimalPlaces);
$result = (string) $fmt->format((float) $rounded); // intentional float
$result = (string)$fmt->format((float)$rounded); // intentional float
if (true === $coloured) {
if (1 === bccomp((string) $rounded, '0')) {
if (1 === bccomp((string)$rounded, '0')) {
return sprintf('<span class="text-success money-positive">%s</span>', $result);
}
if (-1 === bccomp((string) $rounded, '0')) {
if (-1 === bccomp((string)$rounded, '0')) {
return sprintf('<span class="text-danger money-negative">%s</span>', $result);
}
@@ -106,23 +107,21 @@ class Amount
$amount = $journal[$field] ?? '0';
// Log::debug(sprintf('Field is %s, amount is %s', $field, $amount));
// fallback, the transaction has a foreign amount in $currency.
if ($convertToNative && null !== $journal['foreign_amount'] && $currency->id === (int) $journal['foreign_currency_id']) {
if ($convertToNative && null !== $journal['foreign_amount'] && $currency->id === (int)$journal['foreign_currency_id']) {
$amount = $journal['foreign_amount'];
// Log::debug(sprintf('Overruled, amount is now %s', $amount));
}
return (string) $amount;
return (string)$amount;
}
public function convertToNative(?User $user = null): bool
{
if (!$user instanceof User) {
return true === Preferences::get('convert_to_native', false)->data && true === config('cer.enabled');
// Log::debug(sprintf('convertToNative [a]: %s', var_export($result, true)));
}
return true === Preferences::getForUser($user, 'convert_to_native', false)->data && true === config('cer.enabled');
// Log::debug(sprintf('convertToNative [b]: %s', var_export($result, true)));
}
public function getNativeCurrency(): TransactionCurrency
@@ -180,9 +179,9 @@ class Amount
return '0';
}
$amount = $sourceTransaction->{$field} ?? '0';
if ((int) $sourceTransaction->foreign_currency_id === $currency->id) {
if ((int)$sourceTransaction->foreign_currency_id === $currency->id) {
// use foreign amount instead!
$amount = (string) $sourceTransaction->foreign_amount; // hard coded to be foreign amount.
$amount = (string)$sourceTransaction->foreign_amount; // hard coded to be foreign amount.
}
return $amount;

View File

@@ -93,7 +93,7 @@ class Steam
return [];
}
$defaultCurrency = app('amount')->getNativeCurrency();
$defaultCurrency = Amount::getNativeCurrency();
if ($convertToNative) {
if ($defaultCurrency->id === $currency?->id) {
Log::debug(sprintf('Unset [%s] for account #%d (no longer unset "native_balance")', $defaultCurrency->code, $account->id));
@@ -224,7 +224,7 @@ class Steam
$request->subDay()->endOfDay();
Log::debug(sprintf('finalAccountBalanceInRange: Call finalAccountBalance with date/time "%s"', $request->toIso8601String()));
$startBalance = $this->finalAccountBalance($account, $request);
$nativeCurrency = app('amount')->getNativeCurrencyByUserGroup($account->user->userGroup);
$nativeCurrency = Amount::getNativeCurrencyByUserGroup($account->user->userGroup);
$accountCurrency = $this->getAccountCurrency($account);
$hasCurrency = $accountCurrency instanceof TransactionCurrency;
$currency = $accountCurrency ?? $nativeCurrency;
@@ -294,7 +294,7 @@ class Steam
$currentBalance[$entryCurrency->code] ??= '0';
$currentBalance[$entryCurrency->code] = bcadd($sumOfDay, (string) $currentBalance[$entryCurrency->code]);
// if not convert to native, add the amount to "balance", do nothing else.
// if not requested to convert to native, add the amount to "balance", do nothing else.
if (!$convertToNative) {
$currentBalance['balance'] = bcadd((string) $currentBalance['balance'], $sumOfDay);
}
@@ -302,13 +302,13 @@ class Steam
// if there is a request to convert, convert to "native_balance" and use "balance" for whichever amount is in the native currency.
if ($convertToNative) {
$nativeSumOfDay = $converter->convert($entryCurrency, $nativeCurrency, $carbon, $sumOfDay);
$currentBalance['native_balance'] = bcadd((string) $currentBalance['native_balance'], $nativeSumOfDay);
$currentBalance['native_balance'] = bcadd((string) ($currentBalance['native_balance'] ?? '0'), $nativeSumOfDay);
// if it's the same currency as the entry, also add to balance (see other code).
if ($currency->id === $entryCurrency->id) {
$currentBalance['balance'] = bcadd((string) $currentBalance['balance'], $sumOfDay);
}
}
// just set it.
// add to final array.
$balances[$carbonKey] = $currentBalance;
Log::debug(sprintf('Updated entry [%s]', $carbonKey), $currentBalance);
}

View File

@@ -121,6 +121,15 @@ class TransactionGroupTransformer extends AbstractTransformer
if (null !== $transaction['foreign_amount'] && '' !== $transaction['foreign_amount'] && 0 !== bccomp('0', (string) $transaction['foreign_amount'])) {
$foreignAmount = app('steam')->positive($transaction['foreign_amount']);
}
// set native amount to the normal amount if the currency matches.
if ($transaction['native_currency']['id'] ?? null === $transaction['currency_id']) {
$transaction['native_amount'] = $amount;
}
if (array_key_exists('native_amount', $transaction) && null !== $transaction['native_amount']) {
$transaction['native_amount'] = app('steam')->positive($transaction['native_amount']);
}
$type = $this->stringFromArray($transaction, 'transaction_type_type', TransactionTypeEnum::WITHDRAWAL->value);
// must be 0 (int) or NULL

22
composer.lock generated
View File

@@ -10768,16 +10768,16 @@
},
{
"name": "nikic/php-parser",
"version": "v5.5.0",
"version": "v5.6.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9"
"reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9",
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56",
"reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56",
"shasum": ""
},
"require": {
@@ -10820,9 +10820,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0"
},
"time": "2025-05-31T08:24:38+00:00"
"time": "2025-07-27T20:03:57+00:00"
},
{
"name": "phar-io/manifest",
@@ -11065,16 +11065,16 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.19",
"version": "2.1.20",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "473a8c30e450d87099f76313edcbb90852f9afdf"
"reference": "a9ccfef95210f92ba6feea6e8d1eef42b5605499"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/473a8c30e450d87099f76313edcbb90852f9afdf",
"reference": "473a8c30e450d87099f76313edcbb90852f9afdf",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a9ccfef95210f92ba6feea6e8d1eef42b5605499",
"reference": "a9ccfef95210f92ba6feea6e8d1eef42b5605499",
"shasum": ""
},
"require": {
@@ -11119,7 +11119,7 @@
"type": "github"
}
],
"time": "2025-07-21T19:58:24+00:00"
"time": "2025-07-26T20:45:26+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",

View File

@@ -78,8 +78,8 @@ return [
'running_balance_column' => env('USE_RUNNING_BALANCE', false),
// see cer.php for exchange rates feature flag.
],
'version' => 'develop/2025-07-24',
'build_time' => 1753333121,
'version' => 'develop/2025-07-28',
'build_time' => 1753673705,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 26,

View File

@@ -65,6 +65,7 @@ return [
'interest_calc_half-year',
'interest_calc_quarterly',
'spent',
'budgeted',
'administration_owner',
'administration_you',
'administration_role_owner',

202
package-lock.json generated
View File

@@ -402,14 +402,14 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
"integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
"@babel/types": "^7.27.6"
"@babel/types": "^7.28.2"
},
"engines": {
"node": ">=6.9.0"
@@ -1622,9 +1622,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1665,9 +1665,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.28.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
"integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2585,9 +2585,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz",
"integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.0.tgz",
"integrity": "sha512-9f3nSTFI2ivfxc7/tHBHcJ8pRnp8ROrELvsVprlQPVvcZ+j5zztYd+PTJGpyIOAdTvNwNrpCXswKSeoQcyGjMQ==",
"cpu": [
"arm"
],
@@ -2599,9 +2599,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz",
"integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.0.tgz",
"integrity": "sha512-tFZSEhqJ8Yrpe50TzOdeoYi72gi/jsnT7y8Qrozf3cNu28WX+s6I3XzEPUAqoaT9SAS8Xz9AzGTFlxxCH/w20w==",
"cpu": [
"arm64"
],
@@ -2613,9 +2613,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz",
"integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.0.tgz",
"integrity": "sha512-+DikIIs+p6yU2hF51UaWG8BnHbq90X0QIOt5zqSKSZxY+G3qqdLih214e9InJal21af2PuuxkDectetGfbVPJw==",
"cpu": [
"arm64"
],
@@ -2627,9 +2627,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz",
"integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.0.tgz",
"integrity": "sha512-5a+NofhdEB/WimSlFMskbFQn1vqz1FWryYpA99trmZGO6qEmiS0IsX6w4B3d91U878Q2ZQdiaFF1gxX4P147og==",
"cpu": [
"x64"
],
@@ -2641,9 +2641,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz",
"integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.0.tgz",
"integrity": "sha512-igr/RlKPS3OCy4jD3XBmAmo3UAcNZkJSubRsw1JeM8bAbwf15k/3eMZXD91bnjheijJiOJcga3kfCLKjV8IXNg==",
"cpu": [
"arm64"
],
@@ -2655,9 +2655,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz",
"integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.0.tgz",
"integrity": "sha512-MdigWzPSHlQzB1xZ+MdFDWTAH+kcn7UxjEBoOKuaso7z1DRlnAnrknB1mTtNOQ+GdPI8xgExAGwHeqQjntR0Cg==",
"cpu": [
"x64"
],
@@ -2669,9 +2669,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz",
"integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.0.tgz",
"integrity": "sha512-dmZseE0ZwA/4yy1+BwFrDqFTjjNg24GO9xSrb1weVbt6AFkhp5pz1gVS7IMtfIvoWy8yp6q/zN0bKnefRUImvQ==",
"cpu": [
"arm"
],
@@ -2683,9 +2683,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz",
"integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.0.tgz",
"integrity": "sha512-fzhfn6p9Cfm3W8UrWKIa4l7Wfjs/KGdgaswMBBE3KY3Ta43jg2XsPrAtfezHpsRk0Nx+TFuS3hZk/To2N5kFPQ==",
"cpu": [
"arm"
],
@@ -2697,9 +2697,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz",
"integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.0.tgz",
"integrity": "sha512-vVDD+iPDPmJQ5nAQ5Tifq3ywdv60FartglFI8VOCK+hcU9aoG0qlQTsDJP97O5yiTaTqlneZWoARMcVC5nyUoQ==",
"cpu": [
"arm64"
],
@@ -2711,9 +2711,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz",
"integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.0.tgz",
"integrity": "sha512-0d0jx08fzDHCzXqrtCMEEyxKU0SvJrWmUjUDE2/KDQ2UDJql0tfiwYvEx1oHELClKO8CNdE+AGJj+RqXscZpdQ==",
"cpu": [
"arm64"
],
@@ -2725,9 +2725,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz",
"integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.0.tgz",
"integrity": "sha512-XBYu9oW9eKJadWn8M7hkTZsD4yG+RrsTrVEgyKwb4L72cpJjRbRboTG9Lg9fec8MxJp/cfTHAocg4mnismQR8A==",
"cpu": [
"loong64"
],
@@ -2738,10 +2738,10 @@
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz",
"integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==",
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.0.tgz",
"integrity": "sha512-wJaRvcT17PoOK6Ggcfo3nouFlybHvARBS4jzT0PC/lg17fIJHcDS2fZz3sD+iA4nRlho2zE6OGbU0HvwATdokQ==",
"cpu": [
"ppc64"
],
@@ -2753,9 +2753,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz",
"integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.0.tgz",
"integrity": "sha512-GZ5bkMFteAGkcmh8x0Ok4LSa+L62Ez0tMsHPX6JtR0wl4Xc3bQcrFHDiR5DGLEDFtGrXih4Nd/UDaFqs968/wA==",
"cpu": [
"riscv64"
],
@@ -2767,9 +2767,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz",
"integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.0.tgz",
"integrity": "sha512-7CjPw6FflFsVOUfWOrVrREiV3IYXG4RzZ1ZQUaT3BtSK8YXN6x286o+sruPZJESIaPebYuFowmg54ZdrkVBYog==",
"cpu": [
"riscv64"
],
@@ -2781,9 +2781,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz",
"integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.0.tgz",
"integrity": "sha512-nmvnl0ZiuysltcB/cKjUh40Rx4FbSyueERDsl2FLvLYr6pCgSsvGr3SocUT84svSpmloS7f1DRWqtRha74Gi1w==",
"cpu": [
"s390x"
],
@@ -2795,9 +2795,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz",
"integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.0.tgz",
"integrity": "sha512-Cv+moII5C8RM6gZbR3cb21o6rquVDZrN2o81maROg1LFzBz2dZUwIQSxFA8GtGZ/F2KtsqQ2z3eFPBb6akvQNg==",
"cpu": [
"x64"
],
@@ -2809,9 +2809,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz",
"integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.0.tgz",
"integrity": "sha512-PHcMG8DZTM9RCIjp8QIfN0VYtX0TtBPnWOTRurFhoCDoi9zptUZL2k7pCs+5rgut7JAiUsYy+huyhVKPcmxoog==",
"cpu": [
"x64"
],
@@ -2823,9 +2823,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz",
"integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.0.tgz",
"integrity": "sha512-1SI/Rd47e8aQJeFWMDg16ET+fjvCcD/CzeaRmIEPmb05hx+3cCcwIF4ebUag4yTt/D1peE+Mgp0+Po3M358cAA==",
"cpu": [
"arm64"
],
@@ -2837,9 +2837,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz",
"integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.0.tgz",
"integrity": "sha512-JwOCYxmumFDfDhx4kNyz6kTVK3gWzBIvVdMNzQMRDubcoGRDniOOmo6DDNP42qwZx3Bp9/6vWJ+kNzNqXoHmeA==",
"cpu": [
"ia32"
],
@@ -2851,9 +2851,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz",
"integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.0.tgz",
"integrity": "sha512-IPMIfrfkG1GaEXi+JSsQEx8x9b4b+hRZXO7KYc2pKio3zO2/VDXDs6B9Ts/nnO+25Fk1tdAVtUn60HKKPPzDig==",
"cpu": [
"x64"
],
@@ -5694,9 +5694,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.190",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz",
"integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==",
"version": "1.5.191",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz",
"integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==",
"dev": true,
"license": "ISC"
},
@@ -10117,9 +10117,9 @@
}
},
"node_modules/rollup": {
"version": "4.45.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
"integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.0.tgz",
"integrity": "sha512-ONmkT3Ud3IfW15nl7l4qAZko5/2iZ5ALVBDh02ZSZ5IGVLJSYkRcRa3iB58VyEIyoofs9m2xdVrm+lTi97+3pw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10133,26 +10133,26 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.45.1",
"@rollup/rollup-android-arm64": "4.45.1",
"@rollup/rollup-darwin-arm64": "4.45.1",
"@rollup/rollup-darwin-x64": "4.45.1",
"@rollup/rollup-freebsd-arm64": "4.45.1",
"@rollup/rollup-freebsd-x64": "4.45.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.45.1",
"@rollup/rollup-linux-arm-musleabihf": "4.45.1",
"@rollup/rollup-linux-arm64-gnu": "4.45.1",
"@rollup/rollup-linux-arm64-musl": "4.45.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.45.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.45.1",
"@rollup/rollup-linux-riscv64-gnu": "4.45.1",
"@rollup/rollup-linux-riscv64-musl": "4.45.1",
"@rollup/rollup-linux-s390x-gnu": "4.45.1",
"@rollup/rollup-linux-x64-gnu": "4.45.1",
"@rollup/rollup-linux-x64-musl": "4.45.1",
"@rollup/rollup-win32-arm64-msvc": "4.45.1",
"@rollup/rollup-win32-ia32-msvc": "4.45.1",
"@rollup/rollup-win32-x64-msvc": "4.45.1",
"@rollup/rollup-android-arm-eabi": "4.46.0",
"@rollup/rollup-android-arm64": "4.46.0",
"@rollup/rollup-darwin-arm64": "4.46.0",
"@rollup/rollup-darwin-x64": "4.46.0",
"@rollup/rollup-freebsd-arm64": "4.46.0",
"@rollup/rollup-freebsd-x64": "4.46.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.46.0",
"@rollup/rollup-linux-arm-musleabihf": "4.46.0",
"@rollup/rollup-linux-arm64-gnu": "4.46.0",
"@rollup/rollup-linux-arm64-musl": "4.46.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.46.0",
"@rollup/rollup-linux-ppc64-gnu": "4.46.0",
"@rollup/rollup-linux-riscv64-gnu": "4.46.0",
"@rollup/rollup-linux-riscv64-musl": "4.46.0",
"@rollup/rollup-linux-s390x-gnu": "4.46.0",
"@rollup/rollup-linux-x64-gnu": "4.46.0",
"@rollup/rollup-linux-x64-musl": "4.46.0",
"@rollup/rollup-win32-arm64-msvc": "4.46.0",
"@rollup/rollup-win32-ia32-msvc": "4.46.0",
"@rollup/rollup-win32-x64-msvc": "4.46.0",
"fsevents": "~2.3.2"
}
},
@@ -11518,15 +11518,15 @@
}
},
"node_modules/vite": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz",
"integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
"integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.6",
"picomatch": "^4.0.2",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.40.0",
"tinyglobby": "^0.2.14"

View File

@@ -25,7 +25,7 @@ export default class Dashboard {
dashboard(start, end) {
let startStr = format(start, 'y-MM-dd');
let endStr = format(end, 'y-MM-dd');
return api.get('/api/v1/chart/account/dashboard', {params: {fix: true, start: startStr, end: endStr}});
return api.get('/api/v1/chart/account/dashboard', {params: {start: startStr, end: endStr}});
}
expense(start, end) {

View File

@@ -40,24 +40,33 @@ export default () => ({
loadingAccounts: false,
accountList: [],
convertToNative: false,
convertToNativeAvailable: false,
chartOptions: null,
switchConvertToNative() {
this.convertToNative = !this.convertToNative;
setVariable('convertToNative', this.convertToNative);
},
localCacheKey(type) {
return 'ds_accounts_' + type;
},
eventListeners: {
['@convert-to-native.window'](event){
console.log('I heard that! (dashboard/accounts)');
this.convertToNative = event.detail;
this.accountList = [];
chartData = null;
this.loadChart();
this.loadAccounts();
}
},
getFreshData() {
const start = new Date(window.store.get('start'));
const end = new Date(window.store.get('end'));
const chartCacheKey = getCacheKey(this.localCacheKey('chart'), {start: start, end: end})
const chartCacheKey = getCacheKey(this.localCacheKey('chart'), {convertToNative: this.convertToNative, start: start, end: end})
const cacheValid = window.store.get('cacheValid');
let cachedData = window.store.get(chartCacheKey);
if (cacheValid && typeof cachedData !== 'undefined') {
console.log('Generate from cache: ', chartCacheKey);
this.drawChart(this.generateOptions(cachedData));
this.loading = false;
return;
@@ -67,6 +76,7 @@ export default () => ({
this.chartData = response.data;
// cache generated options:
window.store.set(chartCacheKey, response.data);
console.log('Generate FRESH!');
this.drawChart(this.generateOptions(this.chartData));
this.loading = false;
});
@@ -90,18 +100,23 @@ export default () => ({
dataset.label = current.label;
// use the "native" currency code and use the "native_entries" as array
// if (this.convertToNative) {
// currencies.push(current.native_currency_code);
// dataset.currency_code = current.native_currency_code;
// collection = Object.values(current.native_entries);
// yAxis = 'y' + current.native_currency_code;
// }
// if (!this.convertToNative) {
if (this.convertToNative) {
currencies.push(current.native_currency_code);
dataset.currency_code = current.native_currency_code;
if(!current.hasOwnProperty('native_entries')) {
console.error('No native entries ('+this.convertToNative+') found for account: ', current);
}
if(current.hasOwnProperty('native_entries')) {
collection = Object.values(current.native_entries);
}
yAxis = 'y' + current.native_currency_code;
}
if (!this.convertToNative) {
yAxis = 'y' + current.currency_code;
dataset.currency_code = current.currency_code;
currencies.push(current.currency_code);
collection = Object.values(current.entries);
// }
}
dataset.yAxisID = yAxis;
dataset.data = collection;
@@ -273,13 +288,16 @@ export default () => ({
init() {
// console.log('accounts init');
Promise.all([getVariable('viewRange', '1M'), getVariable('convertToNative', false), getVariable('language', 'en_US'),
getConfiguration('cer.enabled', false)
Promise.all([
getVariable('viewRange', '1M'), // 0
getVariable('convert_to_native', false), // 1
getVariable('language', 'en_US'), // 2
getConfiguration('cer.enabled', false) // 3
]).then((values) => {
//console.log('accounts after promises');
this.convertToNative = values[1] && values[3];
this.convertToNativeAvailable = values[3];
afterPromises = true;
//console.log('convertToNative in accounts.js: ', values);
// main dashboard chart:
this.loadChart();
@@ -296,7 +314,7 @@ export default () => ({
this.loadChart();
this.loadAccounts();
});
window.store.observe('convertToNative', () => {
window.store.observe('convert_to_native', () => {
if (!afterPromises) {
return;
}

View File

@@ -35,11 +35,21 @@ export default () => ({
loading: false,
boxData: null,
boxOptions: null,
eventListeners: {
['@convert-to-native.window'](event){
this.convertToNative = event.detail;
this.accountList = [];
console.log('I heard that!');
this.boxData = null;
this.loadBoxes();
}
},
getFreshData() {
const start = new Date(window.store.get('start'));
const end = new Date(window.store.get('end'));
// TODO cache key is hard coded, problem?
const boxesCacheKey = getCacheKey('ds_boxes_data', {start: start, end: end});
const boxesCacheKey = getCacheKey('ds_boxes_data', {convertToNative: this.convertToNative, start: start, end: end});
cleanupCache();
//const cacheValid = window.store.get('cacheValid');
@@ -153,7 +163,7 @@ export default () => ({
init() {
// console.log('boxes init');
// TODO can be replaced by "getVariables"
Promise.all([getVariable('viewRange'), getVariable('convertToNative', false)]).then((values) => {
Promise.all([getVariable('viewRange'), getVariable('convert_to_native', false)]).then((values) => {
// console.log('boxes after promises');
afterPromises = true;
this.convertToNative = values[1];
@@ -167,7 +177,7 @@ export default () => ({
this.boxData = null;
this.loadBoxes();
});
window.store.observe('convertToNative', (newValue) => {
window.store.observe('convert_to_native', (newValue) => {
if (!afterPromises) {
return;
}

View File

@@ -60,7 +60,8 @@ export default () => ({
const start = new Date(window.store.get('start'));
const end = new Date(window.store.get('end'));
const cacheKey = getCacheKey('ds_bdg_chart', {start: start, end: end});
const cacheValid = window.store.get('cacheValid');
//const cacheValid = window.store.get('cacheValid');
const cacheValid = false;
let cachedData = window.store.get(cacheKey);
if (cacheValid && typeof cachedData !== 'undefined') {
@@ -80,7 +81,7 @@ export default () => ({
},
generateOptions(data) {
currencies = [];
let options = getDefaultChartSettings('column');
let options = getDefaultChartSettings('bar');
options.options.locale = window.store.get('locale').replace('_', '-');
options.options.plugins = {
tooltip: {
@@ -94,7 +95,7 @@ export default () => ({
if (label) {
label += ': ';
}
return label + ' ' + formatMoney(context.parsed.y, currencies[context.parsed.x] ?? 'EUR');
return label + ' ' + formatMoney(context.parsed.x, currencies[context.parsed.x] ?? 'EUR');
}
}
}
@@ -103,28 +104,19 @@ export default () => ({
labels: [],
datasets: [
{
label: i18next.t('firefly.budgeted'),
data: [],
borderWidth: 1,
backgroundColor: getColors('budgeted', 'background'),
borderColor: getColors('budgeted', 'border'),
},
{
//label: i18next.t('firefly.budgeted'),
label: i18next.t('firefly.spent'),
data: [],
borderWidth: 1,
stack: 1,
backgroundColor: getColors('spent', 'background'),
borderColor: getColors('spent', 'border'),
},
{
label: i18next.t('firefly.left'),
data: [],
borderWidth: 1,
stack: 1,
backgroundColor: getColors('left', 'background'),
borderColor: getColors('left', 'border'),
},
{
label: i18next.t('firefly.overspent'),
data: [],
borderWidth: 1,
stack: 1,
backgroundColor: getColors('overspent', 'background'),
borderColor: getColors('overspent', 'border'),
}
]
};
@@ -134,24 +126,16 @@ export default () => ({
// // convert to EUR yes no?
let label = current.label + ' (' + current.currency_code + ')';
options.data.labels.push(label);
if (this.convertToNative) {
currencies.push(current.native_currency_code);
// series 0: spent
options.data.datasets[0].data.push(parseFloat(current.native_entries.spent) * -1);
// series 1: left
options.data.datasets[1].data.push(parseFloat(current.native_entries.left));
// series 2: overspent
options.data.datasets[2].data.push(parseFloat(current.native_entries.overspent));
}
if (!this.convertToNative) {
currencies.push(current.currency_code);
// series 0: spent
options.data.datasets[0].data.push(parseFloat(current.entries.spent) * -1);
// series 1: left
options.data.datasets[1].data.push(parseFloat(current.entries.left));
// series 2: overspent
options.data.datasets[2].data.push(parseFloat(current.entries.overspent));
}
// label = current.label + ' (' + current.currency_code + ') b';
// options.data.labels.push(label);
currencies.push(current.currency_code);
// series 0: budgeted
options.data.datasets[0].data.push(parseFloat(current.entries.budgeted));
// series 1: spent
options.data.datasets[1].data.push(parseFloat(current.entries.spent) * -1);
// series 2: overspent
// options.data.datasets[2].data.push(parseFloat(current.entries.overspent));
}
}
// the currency format callback for the Y axis is AlWAYS based on whatever the first currency is.
@@ -160,9 +144,10 @@ export default () => ({
options.options.scales = {
y: {
ticks: {
callback: function (context) {
return formatMoney(context, currencies[0] ?? 'EUR');
}
// callback: function (context) {
// return 'abc';
// return formatMoney(context, currencies[0] ?? 'EUR');
// }
}
}
};
@@ -172,7 +157,7 @@ export default () => ({
init() {
Promise.all([getVariable('convertToNative', false)]).then((values) => {
Promise.all([getVariable('convert_to_native', false)]).then((values) => {
this.convertToNative = values[0];
afterPromises = true;
if (false === this.loading) {
@@ -189,7 +174,7 @@ export default () => ({
this.loadChart();
}
});
window.store.observe('convertToNative', (newValue) => {
window.store.observe('convert_to_native', (newValue) => {
if (!afterPromises) {
return;
}

View File

@@ -183,7 +183,7 @@ export default () => ({
},
init() {
// console.log('categories init');
Promise.all([getVariable('convertToNative', false),]).then((values) => {
Promise.all([getVariable('convert_to_native', false),]).then((values) => {
this.convertToNative = values[0];
afterPromises = true;
this.loadChart();
@@ -195,7 +195,7 @@ export default () => ({
this.chartData = null;
this.loadChart();
});
window.store.observe('convertToNative', (newValue) => {
window.store.observe('convert_to_native', (newValue) => {
if (!afterPromises) {
return;
}

View File

@@ -46,7 +46,8 @@ import {
} from "chart.js";
import 'chartjs-adapter-date-fns';
import {showInternalsButton} from "../../support/page-settings/show-internals-button.js";
import {showWizardButton} from "../../support/page-settings/show-wizard-button.js";
import {setVariable} from "../../store/set-variable.js";
import {getVariable} from "../../store/get-variable.js";
// register things
Chart.register({
@@ -66,7 +67,24 @@ Chart.register({
Legend
});
let index = function () {
return {
convertToNative: false,
saveNativeSettings(event) {
setVariable('convert_to_native', event.currentTarget.checked);
this.$dispatch('convert-to-native', event.currentTarget.checked);
console.log('saveNativeSettings + dispatch.');
},
init() {
Promise.all([getVariable('convert_to_native', false)]).then((values) => {
this.convertToNative = values[0];
});
}
}
};
const comps = {
index,
dates,
boxes,
accounts,

View File

@@ -129,7 +129,7 @@ export default () => ({
init() {
// console.log('piggies init');
apiData = [];
Promise.all([getVariable('convertToNative', false)]).then((values) => {
Promise.all([getVariable('convert_to_native', false)]).then((values) => {
afterPromises = true;
this.convertToNative = values[0];
@@ -144,7 +144,7 @@ export default () => ({
apiData = [];
this.loadPiggyBanks();
});
window.store.observe('convertToNative', (newValue) => {
window.store.observe('convert_to_native', (newValue) => {
if (!afterPromises) {
return;
}

View File

@@ -94,6 +94,7 @@ function getObjectName(type, name, direction, code) {
if ('category' === type && null === name && 'out' === direction) {
return translations.unknown_category + ' (' + translations.out + (convertToNative ? ', ' + code + ')' : ')');
}
// account 4x
if ('account' === type && null === name && 'in' === direction) {
return translations.unknown_source + (convertToNative ? ' (' + code + ')' : '');
@@ -165,6 +166,11 @@ export default () => ({
// properties of the transaction, used in the generation of the chart:
let transaction = group.attributes.transactions[ii];
let currencyCode = this.convertToNative ? transaction.native_currency_code : transaction.currency_code;
if(this.convertToNative && (!transaction.hasOwnProperty('native_amount') || null === transaction.native_amount)) {
// skip this transaction, it has no native amount.
console.error('No native amount for transaction #' + group.id + ' ('+this.convertToNative+')');
continue;
}
let amount = this.convertToNative ? parseFloat(transaction.native_amount) : parseFloat(transaction.amount);
let flowKey;
@@ -312,7 +318,7 @@ export default () => ({
downloadTransactions(params) {
const start = new Date(window.store.get('start'));
const end = new Date(window.store.get('end'));
const cacheKey = getCacheKey(SANKEY_CACHE_KEY, {start: start, end: end});
const cacheKey = getCacheKey(SANKEY_CACHE_KEY, {convertToNative: this.convertToNative, start: start, end: end});
//console.log('Downloading page ' + params.page + '...');
const getter = new Get();
@@ -348,9 +354,10 @@ export default () => ({
init() {
// console.log('sankey init');
transactions = [];
Promise.all([getVariable('convertToNative', false)]).then((values) => {
Promise.all([getVariable('convert_to_native', false)]).then((values) => {
this.convertToNative = values[0];
convertToNative = values[0];
// some translations:
translations.all_money = i18next.t('firefly.all_money');
translations.category = i18next.t('firefly.category');
@@ -378,7 +385,7 @@ export default () => ({
this.transactions = [];
this.loadChart();
});
window.store.observe('convertToNative', (newValue) => {
window.store.observe('convert_to_native', (newValue) => {
if (!afterPromises) {
return;
}

View File

@@ -305,7 +305,7 @@ export default () => ({
},
init() {
Promise.all([getVariable('convertToNative', false)]).then((values) => {
Promise.all([getVariable('convert_to_native', false)]).then((values) => {
this.convertToNative = values[0];
afterPromises = true;
@@ -323,7 +323,7 @@ export default () => ({
this.startSubscriptions();
}
});
window.store.observe('convertToNative', (newValue) => {
window.store.observe('convert_to_native', (newValue) => {
if (!afterPromises) {
return;
}

View File

@@ -71,7 +71,7 @@ let index = function () {
init() {
// TODO need date range.
// TODO handle page number
this.getTransactions(this.page);``
this.getTransactions(this.page);
// Your Javascript code to create the grid
// dataTable = createGrid(document.querySelector('#grid'), gridOptions);

View File

@@ -27,19 +27,29 @@ export function getConfiguration(name, defaultValue = null) {
// to make things available quicker than if the store has to grab it through the API.
// then again, it's not that slow.
if (validCache && window.hasOwnProperty(name)) {
// console.log('Get from window');
console.log('Return configuration "' + name + '" from window: ' + window[name]);
return Promise.resolve(window[name]);
}
// load from store2, if it's present.
const fromStore = window.store.get(name);
if (validCache && typeof fromStore !== 'undefined') {
console.log('Return configuration "' + name + '" from store: ' + fromStore);
return Promise.resolve(fromStore);
}
let getter = (new Get);
return getter.getByName(name).then((response) => {
// console.log('Get "' + name + '" from API');
return Promise.resolve(parseResponse(name, response));
}).catch(() => {
console.log('Return configuration "' + name + '" from API: ' + parseConfigurationResponse(name, response));
return Promise.resolve(parseConfigurationResponse(name, response));
}).catch((error) => {
console.log('Returning "'+name+'" from DEFAULT: ' + defaultValue);
console.warn(error);
return defaultValue;
});
}
export function parseConfigurationResponse(name, response) {
let value = response.data.data.value;
window.store.set(name, value);
return value;
}

View File

@@ -27,16 +27,19 @@ export function getVariable(name, defaultValue = null) {
// to make things available quicker than if the store has to grab it through the API.
// then again, it's not that slow.
if (validCache && window.hasOwnProperty(name)) {
console.log('Returning "'+name+'" from window: ' + window[name]);
return Promise.resolve(window[name]);
}
// load from store2, if it's present.
const fromStore = window.store.get(name);
if (validCache && typeof fromStore !== 'undefined') {
console.log('Returning "'+name+'" from store: ' + fromStore);
return Promise.resolve(fromStore);
}
let getter = (new Get);
return getter.getByName(name).then((response) => {
console.log('Returning "'+name+'" from server: ' + parseResponse(name, response));
return Promise.resolve(parseResponse(name, response));
}).catch((error) => {
if('' === defaultValue) {
@@ -47,6 +50,7 @@ export function getVariable(name, defaultValue = null) {
// POST it and then return it anyway.
let poster = (new Post);
return poster.post(name, defaultValue).then((response) => {
console.log('Returning "'+name+'" from POST: ' + parseResponse(name, response));
return Promise.resolve(parseResponse(name, response));
});
});

View File

@@ -28,7 +28,7 @@ export function setVariable(name, value = null) {
// then again, it's not that slow.
// set in window.x
// window[name] = value;
window[name] = value;
// set in store:
window.store.set(name, value);
@@ -36,11 +36,14 @@ export function setVariable(name, value = null) {
// post to user preferences (because why not):
let putter = new Put();
putter.put(name, value).then((response) => {
}).catch(() => {
console.log('set "'+name+'" to value: ', value);
}).catch((error) => {
console.error(error);
// preference does not exist (yet).
// POST it
let poster = (new Post);
poster.post(name, value).then((response) => {
poster.post(name, value).then((response) => {
console.log('POST "'+name+'" to value: ', value);
});
});
}

View File

@@ -62,6 +62,36 @@ function getDefaultChartSettings(type) {
},
};
}
if('bar' === type) {
return {
type: 'bar',
data: {
labels: [],
datasets: [],
},
options: {
maintainAspectRatio: false,
indexAxis: 'y',
// Elements options apply to all the options unless overridden in a dataset
// In this case, we are setting the border of each horizontal bar to be 2px wide
elements: {
bar: {
borderWidth: 2,
}
},
responsive: true,
plugins: {
legend: {
position: 'right',
},
title: {
display: true,
text: 'Chart.js Horizontal Bar Chart'
}
}
},
};
}
if ('line' === type) {
return {
options: {

View File

@@ -92,6 +92,14 @@ function getColors(type, field) {
backgroundColor: background.rgbString(),
};
break;
case 'budgeted':
background = new Color(green.rgbString());
background.lighten(0.38);
colors = {
borderColor: green.rgbString(),
backgroundColor: background.rgbString(),
};
break;
case 'overspent':
background = new Color(red.rgbString());
background.lighten(0.22);

View File

@@ -21,6 +21,14 @@
import {format} from "date-fns";
export default function (amount, currencyCode) {
if( (typeof amount !== 'number' && typeof amount !== 'string') || isNaN(amount)) {
console.warn('format-money: amount is not a number:', amount);
return '';
}
if(typeof currencyCode !== 'string' || currencyCode.length !== 3) {
console.warn('format-money: currencyCode is not a valid ISO 4217 code:', currencyCode);
return '';
}
let locale = window.__localeId__.replace('_', '-');
return Intl.NumberFormat(locale, {

View File

@@ -7,7 +7,7 @@
@include('partials.dashboard.boxes')
<!-- row with account, budget and category data -->
<div class="row mb-2" x-data="accounts">
<div class="row mb-2" x-data="accounts" x-bind="eventListeners">
<!-- column with 3 charts -->
<div class="col-xl-8 col-lg-12 col-sm-12 col-xs-12">
<!-- row with account chart -->
@@ -37,11 +37,11 @@
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title"><a href="#" title="Something">recurring? rules? tags?</a></h3>
<h3 class="card-title"><a href="#" title="Something">Income + sum</a></h3>
</div>
<div class="card-body">
<p>
TODO
List of income + sum
</p>
</div>
</div>
@@ -49,7 +49,7 @@
</div>
</div>
<div class="modal fade" id="internalsModal" tabindex="-1" aria-labelledby="internalsModalLabel"
<div x-data="index" class="modal fade" id="internalsModal" tabindex="-1" aria-labelledby="internalsModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
@@ -58,9 +58,17 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
Buttons.
</p>
<div class="row mb-3">
<label class="col-sm-4 col-form-label">Convert to native</label>
<div class="col-sm-8">
<div class="form-check form-switch form-check-inline">
<label>
<input class="form-check-input" x-model="convertToNative" type="checkbox" @change="saveNativeSettings"> <span
>Yes no</span>
</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ __('firefly.close') }}</button>

View File

@@ -9,24 +9,6 @@
<div class="card-body p-0" style="position: relative;height:400px;">
<canvas id="account-chart"></canvas>
</div>
<template x-if="convertToNativeAvailable">
<div class="card-footer text-end">
<template x-if="convertToNative">
<button type="button" @click="switchConvertToNative"
class="btn btn-outline-info btm-sm">
<span
class="fa-solid fa-comments-dollar"></span> {{ __('firefly.disable_auto_convert') }}
</button>
</template>
<template x-if="!convertToNative">
<button type="button" @click="switchConvertToNative"
class="btn btn-outline-info btm-sm">
<span
class="fa-solid fa-comments-dollar"></span> {{ __('firefly.enable_auto_convert') }}
</button>
</template>
</div>
</template>
</div>
</div>

View File

@@ -1,4 +1,4 @@
<div class="row mb-2" x-data="boxes">
<div class="row mb-2" x-data="boxes" x-bind="eventListeners">
<div class="col-xl-3 col-lg-6 col-md-12 col-sm-12">
<div class="small-box text-bg-primary">
<div class="inner balance-box">

View File

@@ -10,6 +10,7 @@
</div>
<div class="card-body">
<div class="row mb-2">
<!--
<template x-for="pie in group.payment_info">
<div :class='group.col_size'>
<canvas :id='"pie_" + group.id + "_" + pie.currency_code'
@@ -17,6 +18,8 @@
x-init="drawPieChart(group.id, group.title, pie)"></canvas>
</div>
</template>
-->
Here was chart.
</div>
<div class="row mb-2">
<table class="table table-striped table-hover">