Compare commits

...

43 Commits

Author SHA1 Message Date
github-actions[bot]
f3b387fc22 Merge pull request #10673 from firefly-iii/release-1753936796
🤖 Automatically merge the PR into the develop branch.
2025-07-31 06:40:05 +02:00
JC5
03904ffcde 🤖 Auto commit for release 'develop' on 2025-07-31 2025-07-31 06:39:56 +02:00
James Cole
a7973190c2 Start improving bill overview. 2025-07-30 20:35:28 +02:00
James Cole
671ff95f22 Improved subscription overview 2025-07-30 18:57:32 +02:00
github-actions[bot]
01181ceea9 Merge pull request #10672 from firefly-iii/release-1753879077
🤖 Automatically merge the PR into the develop branch.
2025-07-30 14:38:08 +02:00
JC5
97643639d1 🤖 Auto commit for release 'develop' on 2025-07-30 2025-07-30 14:37:57 +02:00
James Cole
424783c47b Add convertToNative to cache key. 2025-07-30 14:33:26 +02:00
James Cole
ea0ced70b2 Clean up sankey. 2025-07-30 11:24:00 +02:00
James Cole
1a633e64ef Category chart will also convert. 2025-07-30 10:20:35 +02:00
James Cole
30da3f4399 Also include budget in currency conversion. 2025-07-30 09:59:52 +02:00
github-actions[bot]
6bdff95d87 Merge pull request #10671 from firefly-iii/release-1753858574
🤖 Automatically merge the PR into the develop branch.
2025-07-30 08:56:22 +02:00
JC5
895ae279d5 🤖 Auto commit for release 'develop' on 2025-07-30 2025-07-30 08:56:14 +02:00
James Cole
8b57f45963 Return the promise. 2025-07-30 08:37:19 +02:00
James Cole
a2d2b7edd3 Add warning to translations. 2025-07-30 07:27:00 +02:00
James Cole
3d65f00c6e Correct tag date and add warning. 2025-07-30 07:26:50 +02:00
James Cole
333004c4d9 Upgrade to font awesome 7, make sure chart includes currencies outside of limits. 2025-07-30 06:59:58 +02:00
James Cole
7451659824 Merge pull request #10661 from firefly-iii/dependabot/npm_and_yarn/develop/cross-env-10.0.0
Bump cross-env from 7.0.3 to 10.0.0
2025-07-28 20:44:31 +02:00
mergify[bot]
ec89c23ace Merge branch 'develop' into dependabot/npm_and_yarn/develop/cross-env-10.0.0 2025-07-28 05:08:38 +00:00
James Cole
92d07d346f Merge pull request #10660 from firefly-iii/dependabot/npm_and_yarn/develop/fortawesome/fontawesome-free-7.0.0
Bump @fortawesome/fontawesome-free from 6.7.2 to 7.0.0
2025-07-28 07:08:02 +02:00
mergify[bot]
a7ac894af2 Merge branch 'develop' into dependabot/npm_and_yarn/develop/cross-env-10.0.0 2025-07-28 04:39:49 +00:00
dependabot[bot]
20f89e3a7c Bump cross-env from 7.0.3 to 10.0.0
Bumps [cross-env](https://github.com/kentcdodds/cross-env) from 7.0.3 to 10.0.0.
- [Release notes](https://github.com/kentcdodds/cross-env/releases)
- [Changelog](https://github.com/kentcdodds/cross-env/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kentcdodds/cross-env/compare/v7.0.3...v10.0.0)

---
updated-dependencies:
- dependency-name: cross-env
  dependency-version: 10.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 04:39:15 +00:00
mergify[bot]
5d2f11c3c7 Merge branch 'develop' into dependabot/npm_and_yarn/develop/fortawesome/fontawesome-free-7.0.0 2025-07-28 04:38:58 +00:00
dependabot[bot]
1eea79e431 Bump @fortawesome/fontawesome-free from 6.7.2 to 7.0.0
Bumps [@fortawesome/fontawesome-free](https://github.com/FortAwesome/Font-Awesome) from 6.7.2 to 7.0.0.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/7.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.7.2...7.0.0)

---
updated-dependencies:
- dependency-name: "@fortawesome/fontawesome-free"
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 04:38:22 +00:00
James Cole
1aaaac67ca Merge pull request #10659 from firefly-iii/dependabot/composer/develop/phpstan/phpstan-2.1.20
Bump phpstan/phpstan from 2.1.19 to 2.1.20
2025-07-28 06:38:11 +02:00
mergify[bot]
1eb86639c9 Merge branch 'develop' into dependabot/composer/develop/phpstan/phpstan-2.1.20 2025-07-28 04:31:47 +00:00
dependabot[bot]
28b911c4ee Bump phpstan/phpstan from 2.1.19 to 2.1.20
Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan) from 2.1.19 to 2.1.20.
- [Release notes](https://github.com/phpstan/phpstan/releases)
- [Changelog](https://github.com/phpstan/phpstan/blob/2.1.x/CHANGELOG.md)
- [Commits](https://github.com/phpstan/phpstan/compare/2.1.19...2.1.20)

---
updated-dependencies:
- dependency-name: phpstan/phpstan
  dependency-version: 2.1.20
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 04:31:06 +00:00
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
github-actions[bot]
bf6e1cb0e1 Merge pull request #10642 from firefly-iii/release-1753333227
🤖 Automatically merge the PR into the develop branch.
2025-07-24 07:00:35 +02:00
JC5
6a53f5031c 🤖 Auto commit for release 'develop' on 2025-07-24 2025-07-24 07:00:27 +02:00
James Cole
ae15ec01e8 Merge branch 'main' into develop 2025-07-24 06:56:07 +02:00
James Cole
fe3c7c47c4 Filter list of bills. 2025-07-24 06:55:53 +02:00
James Cole
68b934010c Merge pull request #10640 from firefly-iii/dependabot/npm_and_yarn/npm_and_yarn-25988072ba
Bump the npm_and_yarn group across 1 directory with 2 updates
2025-07-23 19:37:09 +02:00
dependabot[bot]
22852bd238 Bump the npm_and_yarn group across 1 directory with 2 updates
Bumps the npm_and_yarn group with 1 update in the / directory: [axios](https://github.com/axios/axios).


Updates `axios` from 1.10.0 to 1.11.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.10.0...v1.11.0)

Updates `form-data` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.3...v4.0.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.11.0
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-23 16:51:30 +00:00
50 changed files with 1284 additions and 719 deletions

View File

@@ -406,20 +406,20 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.84.0",
"version": "v3.85.1",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "38dad0767bf2a9b516b976852200ae722fe984ca"
"reference": "2fb6d7f6c3398dca5786a1635b27405d73a417ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/38dad0767bf2a9b516b976852200ae722fe984ca",
"reference": "38dad0767bf2a9b516b976852200ae722fe984ca",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2fb6d7f6c3398dca5786a1635b27405d73a417ba",
"reference": "2fb6d7f6c3398dca5786a1635b27405d73a417ba",
"shasum": ""
},
"require": {
"clue/ndjson-react": "^1.0",
"clue/ndjson-react": "^1.3",
"composer/semver": "^3.4",
"composer/xdebug-handler": "^3.0.5",
"ext-filter": "*",
@@ -429,12 +429,12 @@
"fidry/cpu-core-counter": "^1.2",
"php": "^7.4 || ^8.0",
"react/child-process": "^0.6.6",
"react/event-loop": "^1.0",
"react/promise": "^2.11 || ^3.0",
"react/socket": "^1.0",
"react/stream": "^1.0",
"react/event-loop": "^1.5",
"react/promise": "^3.2",
"react/socket": "^1.16",
"react/stream": "^1.4",
"sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0",
"symfony/console": "^5.4.45 || ^6.4.13 || ^7.0",
"symfony/console": "^5.4.47 || ^6.4.13 || ^7.0",
"symfony/event-dispatcher": "^5.4.45 || ^6.4.13 || ^7.0",
"symfony/filesystem": "^5.4.45 || ^6.4.13 || ^7.0",
"symfony/finder": "^5.4.45 || ^6.4.17 || ^7.0",
@@ -499,7 +499,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.84.0"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.85.1"
},
"funding": [
{
@@ -507,7 +507,7 @@
"type": "github"
}
],
"time": "2025-07-15T18:21:57+00:00"
"time": "2025-07-29T22:22:50+00:00"
},
{
"name": "psr/container",

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

@@ -36,6 +36,7 @@ use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Budget\OperationsRepositoryInterface;
use FireflyIII\Support\Http\Api\CleansChartData;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
@@ -77,6 +78,8 @@ class BudgetController extends Controller
/**
* TODO see autocomplete/accountcontroller
*
* @throws FireflyException
*/
public function dashboard(DateRequest $request): JsonResponse
{
@@ -107,23 +110,41 @@ class BudgetController extends Controller
private function processBudget(Budget $budget, Carbon $start, Carbon $end): array
{
// get all limits:
$limits = $this->blRepository->getBudgetLimits($budget, $start, $end);
$rows = [];
$limits = $this->blRepository->getBudgetLimits($budget, $start, $end);
$rows = [];
$spent = $this->opsRepository->listExpenses($start, $end, null, new Collection([$budget]));
$expenses = $this->processExpenses($budget->id, $spent, $start, $end);
/**
* @var int $currencyId
* @var array $row
*/
foreach ($expenses as $currencyId => $row) {
// budgeted, left and overspent are now 0.
$limit = $this->filterLimit($currencyId, $limits);
if (null !== $limit) {
$row['budgeted'] = $limit->amount;
$row['left'] = bcsub($row['budgeted'], bcmul($row['spent'], '-1'));
$row['overspent'] = bcmul($row['left'], '-1');
$row['left'] = 1 === bccomp($row['left'], '0') ? $row['left'] : '0';
$row['overspent'] = 1 === bccomp($row['overspent'], '0') ? $row['overspent'] : '0';
}
$rows[] = $row;
}
// if no limits
if (0 === $limits->count()) {
// return as a single item in an array
$rows = $this->noBudgetLimits($budget, $start, $end);
}
if ($limits->count() > 0) {
$rows = $this->budgetLimits($budget, $limits);
}
// if (0 === $limits->count()) {
// return as a single item in an array
// $rows = $this->noBudgetLimits($budget, $start, $end);
// }
// is always an array
$return = [];
$return = [];
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 +152,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 +181,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 +194,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 +214,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,16 +261,52 @@ 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']));
}
}
return $result;
}
private function filterLimit(int $currencyId, Collection $limits): ?BudgetLimit
{
$amount = '0';
$limit = null;
$converter = new ExchangeRateConverter();
/** @var BudgetLimit $current */
foreach ($limits as $current) {
if (true === $this->convertToNative) {
if ($current->transaction_currency_id === $this->nativeCurrency->id) {
// simply add it.
$amount = bcadd($amount, (string)$current->amount);
Log::debug(sprintf('Set amount in limit to %s', $amount));
}
if ($current->transaction_currency_id !== $this->nativeCurrency->id) {
// convert and then add it.
$converted = $converter->convert($current->transactionCurrency, $this->nativeCurrency, $limit->start_date, $limit->amount);
$amount = bcadd($amount, $converted);
Log::debug(sprintf('Budgeted in limit #%d: %s %s, converted to %s %s', $current->id, $current->transactionCurrency->code, $current->amount, $this->nativeCurrency->code, $converted));
Log::debug(sprintf('Set amount in limit to %s', $amount));
}
}
if ($current->transaction_currency_id === $currencyId) {
$limit = $current;
}
}
if (null !== $limit && true === $this->convertToNative) {
// convert and add all amounts.
$limit->amount = app('steam')->positive($amount);
Log::debug(sprintf('Final amount in limit with converted amount %s', $limit->amount));
}
return $limit;
}
}

View File

@@ -25,17 +25,20 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Chart;
use Carbon\Carbon;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V2\Request\Generic\DateRequest;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Support\Http\Api\CleansChartData;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
/**
* Class BudgetController
@@ -45,6 +48,8 @@ class CategoryController extends Controller
use CleansChartData;
use ValidatesUserGroupTrait;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
private AccountRepositoryInterface $accountRepos;
private CurrencyRepositoryInterface $currencyRepos;
@@ -79,9 +84,10 @@ class CategoryController extends Controller
/** @var Carbon $end */
$end = $this->parameters->get('end');
$accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]);
$accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::ASSET->value]);
$currencies = [];
$return = [];
$converter = new ExchangeRateConverter();
// get journals for entire period:
/** @var GroupCollectorInterface $collector */
@@ -93,20 +99,40 @@ class CategoryController extends Controller
/** @var array $journal */
foreach ($journals as $journal) {
$currencyId = (int) $journal['currency_id'];
$currency = $currencies[$currencyId] ?? $this->currencyRepos->find($currencyId);
$currencies[$currencyId] = $currency;
$categoryName = $journal['category_name'] ?? (string) trans('firefly.no_category');
$amount = app('steam')->positive($journal['amount']);
$key = sprintf('%s-%s', $categoryName, $currency->code);
// find journal:
$journalCurrencyId = (int)$journal['currency_id'];
$currency = $currencies[$journalCurrencyId] ?? $this->currencyRepos->find($journalCurrencyId);
$currencies[$journalCurrencyId] = $currency;
$currencyId = (int)$currency->id;
$currencyName = (string)$currency->name;
$currencyCode = (string)$currency->code;
$currencySymbol = (string)$currency->symbol;
$currencyDecimalPlaces = (int)$currency->decimal_places;
$amount = app('steam')->positive($journal['amount']);
// overrule if necessary:
if ($this->convertToNative && $journalCurrencyId !== $this->nativeCurrency->id) {
$currencyId = (int)$this->nativeCurrency->id;
$currencyName = (string)$this->nativeCurrency->name;
$currencyCode = (string)$this->nativeCurrency->code;
$currencySymbol = (string)$this->nativeCurrency->symbol;
$currencyDecimalPlaces = (int)$this->nativeCurrency->decimal_places;
$convertedAmount = $converter->convert($currency, $this->nativeCurrency, $journal['date'], $amount);
Log::debug(sprintf('Converted %s %s to %s %s', $journal['currency_code'], $amount, $this->nativeCurrency->code, $convertedAmount));
$amount = $convertedAmount;
}
$categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category');
$key = sprintf('%s-%s', $categoryName, $currencyCode);
// create arrays
$return[$key] ??= [
'label' => $categoryName,
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_name' => $currency->name,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'currency_id' => (string)$currencyId,
'currency_code' => $currencyCode,
'currency_name' => $currencyName,
'currency_symbol' => $currencySymbol,
'currency_decimal_places' => $currencyDecimalPlaces,
'period' => null,
'start' => $start->toAtomString(),
'end' => $end->toAtomString(),
@@ -114,12 +140,12 @@ class CategoryController extends Controller
];
// add monies
$return[$key]['amount'] = bcadd($return[$key]['amount'], (string) $amount);
$return[$key]['amount'] = bcadd($return[$key]['amount'], (string)$amount);
}
$return = array_values($return);
// order by amount
usort($return, static fn (array $a, array $b) => (float) $a['amount'] < (float) $b['amount'] ? 1 : -1);
usort($return, static fn (array $a, array $b) => (float)$a['amount'] < (float)$b['amount'] ? 1 : -1);
return response()->json($this->clean($return));
}

View File

@@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Bill;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\SubscriptionEnrichment;
use FireflyIII\Transformers\BillTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
@@ -76,6 +78,15 @@ class ShowController extends Controller
$bills = $bills->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
$paginator = new LengthAwarePaginator($bills, $count, $pageSize, $this->parameters->get('page'));
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new SubscriptionEnrichment();
$enrichment->setUser($admin);
$enrichment->setConvertToNative($this->convertToNative);
$enrichment->setNative($this->nativeCurrency);
$bills = $enrichment->enrich($bills);
/** @var BillTransformer $transformer */
$transformer = app(BillTransformer::class);
$transformer->setParameters($this->parameters);

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

@@ -154,12 +154,18 @@ class TagController extends Controller
*/
public function index(TagRepositoryInterface $repository)
{
// start with oldest tag
// start with the oldest tag
$first = session('first', today()) ?? today();
$oldestTagDate = $repository->oldestTag() instanceof Tag ? $repository->oldestTag()->date : clone $first;
$newestTagDate = $repository->newestTag() instanceof Tag ? $repository->newestTag()->date : today();
$oldestTagDate->startOfYear();
$newestTagDate->endOfYear();
if ($oldestTagDate->year < 1970) {
$oldestTagDate = Carbon::create(1970, 1, 1, 0, 0, 0, config('app.timezone'));
request()->session()->flash('error', trans('firefly.bad_date_transaction'));
}
$tags = [];
$tags['no-date'] = $repository->getTagsInYear(null);

View File

@@ -24,14 +24,16 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\Budget;
use Deprecated;
use Carbon\Carbon;
use Deprecated;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\Budget;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Report\Summarizer\TransactionSummarizer;
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
@@ -55,17 +57,17 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
$total = '0';
$count = 0;
foreach ($budget->budgetlimits as $limit) {
$diff = (int) $limit->start_date->diffInDays($limit->end_date, true);
$diff = (int)$limit->start_date->diffInDays($limit->end_date, true);
$diff = 0 === $diff ? 1 : $diff;
$amount = $limit->amount;
$perDay = bcdiv((string) $amount, (string) $diff);
$perDay = bcdiv((string)$amount, (string)$diff);
$total = bcadd($total, $perDay);
++$count;
app('log')->debug(sprintf('Found %d budget limits. Per day is %s, total is %s', $count, $perDay, $total));
}
$avg = $total;
if ($count > 0) {
$avg = bcdiv($total, (string) $count);
$avg = bcdiv($total, (string)$count);
}
app('log')->debug(sprintf('%s / %d = %s = average.', $total, $count, $avg));
@@ -93,9 +95,9 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
/** @var array $journal */
foreach ($journals as $journal) {
// prep data array for currency:
$budgetId = (int) $journal['budget_id'];
$budgetId = (int)$journal['budget_id'];
$budgetName = $journal['budget_name'];
$currencyId = (int) $journal['currency_id'];
$currencyId = (int)$journal['currency_id'];
$key = sprintf('%d-%d', $budgetId, $currencyId);
$data[$key] ??= [
@@ -110,7 +112,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
'entries' => [],
];
$date = $journal['date']->format($carbonFormat);
$data[$key]['entries'][$date] = bcadd($data[$key]['entries'][$date] ?? '0', (string) $journal['amount']);
$data[$key]['entries'][$date] = bcadd($data[$key]['entries'][$date] ?? '0', (string)$journal['amount']);
}
return $data;
@@ -124,7 +126,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
public function listExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $budgets = null): array
{
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
if ($accounts instanceof Collection && $accounts->count() > 0) {
$collector->setAccounts($accounts);
@@ -136,15 +138,41 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
$collector->setBudgets($this->getBudgets());
}
$collector->withBudgetInformation()->withAccountInformation()->withCategoryInformation();
$journals = $collector->getExtractedJournals();
$array = [];
$journals = $collector->getExtractedJournals();
$array = [];
// if needs conversion to native.
$convertToNative = Amount::convertToNative($this->user);
$nativeCurrency = Amount::getNativeCurrencyByUserGroup($this->userGroup);
$currencyId = (int) $nativeCurrency->id;
$currencyCode = $nativeCurrency->code;
$currencyName = $nativeCurrency->name;
$currencySymbol = $nativeCurrency->symbol;
$currencyDecimalPlaces = $nativeCurrency->decimal_places;
$converter = new ExchangeRateConverter();
$currencies = [
$currencyId => $nativeCurrency,
];
foreach ($journals as $journal) {
$currencyId = (int) $journal['currency_id'];
$budgetId = (int) $journal['budget_id'];
$budgetName = (string) $journal['budget_name'];
$amount = app('steam')->negative($journal['amount']);
$journalCurrencyId = (int)$journal['currency_id'];
if (false === $convertToNative) {
$currencyId = $journalCurrencyId;
$currencyName = $journal['currency_name'];
$currencySymbol = $journal['currency_symbol'];
$currencyCode = $journal['currency_code'];
$currencyDecimalPlaces = $journal['currency_decimal_places'];
}
if (true === $convertToNative && $journalCurrencyId !== $currencyId) {
$currencies[$journalCurrencyId] ??= TransactionCurrency::find($journalCurrencyId);
$amount = $converter->convert($currencies[$journalCurrencyId], $nativeCurrency, $journal['date'], $amount);
}
// catch "no category" entries.
$budgetId = (int)$journal['budget_id'];
$budgetName = (string)$journal['budget_name'];
// catch "no budget" entries.
if (0 === $budgetId) {
continue;
}
@@ -153,10 +181,10 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
$array[$currencyId] ??= [
'budgets' => [],
'currency_id' => $currencyId,
'currency_name' => $journal['currency_name'],
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
'currency_decimal_places' => $journal['currency_decimal_places'],
'currency_name' => $currencyName,
'currency_symbol' => $currencySymbol,
'currency_code' => $currencyCode,
'currency_decimal_places' => $currencyDecimalPlaces,
];
// info about the categories:
@@ -168,9 +196,9 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
// add journal to array:
// only a subset of the fields.
$journalId = (int) $journal['transaction_journal_id'];
$journalId = (int)$journal['transaction_journal_id'];
$array[$currencyId]['budgets'][$budgetId]['transaction_journals'][$journalId] = [
'amount' => app('steam')->negative($journal['amount']),
'amount' => $amount,
'destination_account_id' => $journal['destination_account_id'],
'destination_account_name' => $journal['destination_account_name'],
'source_account_id' => $journal['source_account_id'],

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

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Support\JsonApi\Enrichments;
use FireflyIII\Models\Bill;
use FireflyIII\Models\Note;
use FireflyIII\Models\ObjectGroup;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class SubscriptionEnrichment implements EnrichmentInterface
{
private User $user;
private UserGroup $userGroup;
private Collection $collection;
private bool $convertToNative = false;
private array $subscriptionIds = [];
private array $objectGroups = [];
private array $mappedObjects = [];
private array $notes = [];
private TransactionCurrency $nativeCurrency;
public function enrich(Collection $collection): Collection
{
$this->collection = $collection;
$this->collectSubscriptionIds();
$this->collectNotes();
$this->collectObjectGroups();
$notes = $this->notes;
$objectGroups = $this->objectGroups;
$this->collection = $this->collection->map(function (Bill $item) use ($notes, $objectGroups) {
$id = (int) $item->id;
$currency = $item->transactionCurrency;
$meta = [
'notes' => null,
'object_group_id' => null,
'object_group_title' => null,
'object_group_order' => null,
];
$amounts = [
'amount_min' => Steam::bcround($item->amount_min, $currency->decimal_places),
'amount_max' => Steam::bcround($item->amount_max, $currency->decimal_places),
'average' => Steam::bcround(bcdiv(bcadd($item->amount_min, $item->amount_max), '2'), $currency->decimal_places),
];
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
$meta['object_group_id'] = $objectGroups[$key]['id'];
$meta['object_group_title'] = $objectGroups[$key]['title'];
$meta['object_group_order'] = $objectGroups[$key]['order'];
}
// Add notes if available.
if (array_key_exists($item->id, $notes)) {
$meta['notes'] = $notes[$item->id];
}
// Convert amounts to native currency if needed
if ($this->convertToNative && $item->currency_id !== $this->nativeCurrency->id) {
$converter = new ExchangeRateConverter();
$amounts = [
'amount_min' => Steam::bcround($converter->convert($item->transactionCurrency, $this->nativeCurrency, today(), $item->amount_min), $this->nativeCurrency->decimal_places),
'amount_max' => Steam::bcround($converter->convert($item->transactionCurrency, $this->nativeCurrency, today(), $item->amount_max), $this->nativeCurrency->decimal_places),
];
$amounts['average'] = Steam::bcround(bcdiv(bcadd($amounts['amount_min'], $amounts['amount_max']), '2'), $this->nativeCurrency->decimal_places);
}
$item->amounts = $amounts;
$item->meta = $meta;
return $item;
});
return $collection;
}
public function enrichSingle(array|Model $model): array|Model
{
Log::debug(__METHOD__);
$collection = new Collection([$model]);
$collection = $this->enrich($collection);
return $collection->first();
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->subscriptionIds)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Bill::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int) $note['noteable_id']] = (string) $note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
public function setUser(User $user): void
{
$this->user = $user;
$this->userGroup = $user->userGroup;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
public function setConvertToNative(bool $convertToNative): void
{
$this->convertToNative = $convertToNative;
}
public function setNative(TransactionCurrency $nativeCurrency): void
{
$this->nativeCurrency = $nativeCurrency;
}
private function collectSubscriptionIds(): void
{
/** @var Bill $bill */
foreach ($this->collection as $bill) {
$this->subscriptionIds[] = (int) $bill->id;
}
$this->subscriptionIds = array_unique($this->subscriptionIds);
}
private function collectObjectGroups(): void
{
$set = DB::table('object_groupables')
->whereIn('object_groupable_id', $this->subscriptionIds)
->where('object_groupable_type', Bill::class)
->get(['object_groupable_id', 'object_group_id'])
;
$ids = array_unique($set->pluck('object_group_id')->toArray());
foreach ($set as $entry) {
$this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id;
}
$groups = ObjectGroup::whereIn('id', $ids)->get(['id', 'title', 'order'])->toArray();
foreach ($groups as $group) {
$group['id'] = (int) $group['id'];
$group['order'] = (int) $group['order'];
$this->objectGroups[(int)$group['id']] = $group;
}
}
}

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

@@ -116,14 +116,14 @@ class AccountTransformer extends AbstractTransformer
$balances = [];
$balances[]
= [
'type' => 'current',
'amount' => $currentBalance,
'currency_id' => $account->meta['currency_id'] ?? null,
'currency_code' => $account->meta['currency']?->code,
'currency_symbol' => $account->meta['currency']?->symbol,
'currency_decimal_places' => $account->meta['currency']?->decimal_places,
'date' => $date->toAtomString(),
];
'type' => 'current',
'amount' => $currentBalance,
'currency_id' => $account->meta['currency_id'] ?? null,
'currency_code' => $account->meta['currency']?->code,
'currency_symbol' => $account->meta['currency']?->symbol,
'currency_decimal_places' => $account->meta['currency']?->decimal_places,
'date' => $date->toAtomString(),
];
if (null !== $nativeCurrentBalance) {
$balances[] = [
'type' => 'native_current',

View File

@@ -27,7 +27,6 @@ namespace FireflyIII\Transformers;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use FireflyIII\Models\Bill;
use FireflyIII\Models\ObjectGroup;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
@@ -44,7 +43,7 @@ class BillTransformer extends AbstractTransformer
{
private readonly BillDateCalculator $calculator;
private readonly bool $convertToNative;
private readonly TransactionCurrency $default;
private readonly TransactionCurrency $native;
private readonly BillRepositoryInterface $repository;
/**
@@ -54,7 +53,7 @@ class BillTransformer extends AbstractTransformer
{
$this->repository = app(BillRepositoryInterface::class);
$this->calculator = app(BillDateCalculator::class);
$this->default = Amount::getNativeCurrency();
$this->native = Amount::getNativeCurrency();
$this->convertToNative = Amount::convertToNative();
}
@@ -66,28 +65,14 @@ class BillTransformer extends AbstractTransformer
*/
public function transform(Bill $bill): array
{
$default = $this->parameters->get('defaultCurrency') ?? $this->default;
$paidData = $this->paidData($bill);
$lastPaidDate = $this->getLastPaidDate($paidData);
$start = $this->parameters->get('start') ?? today()->subYears(10);
$end = $this->parameters->get('end') ?? today()->addYears(10);
$payDates = $this->calculator->getPayDates($start, $end, $bill->date, $bill->repeat_freq, $bill->skip, $lastPaidDate);
$currency = $bill->transactionCurrency;
$notes = $this->repository->getNoteText($bill);
$notes = '' === $notes ? null : $notes;
$objectGroupId = null;
$objectGroupOrder = null;
$objectGroupTitle = null;
$this->repository->setUser($bill->user);
/** @var null|ObjectGroup $objectGroup */
$objectGroup = $bill->objectGroups->first();
if (null !== $objectGroup) {
$objectGroupId = $objectGroup->id;
$objectGroupOrder = $objectGroup->order;
$objectGroupTitle = $objectGroup->title;
}
$paidDataFormatted = [];
$payDatesFormatted = [];
@@ -156,15 +141,16 @@ class BillTransformer extends AbstractTransformer
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'native_currency_id' => null === $default ? null : (string)$default->id,
'native_currency_code' => $default?->code,
'native_currency_symbol' => $default?->symbol,
'native_currency_decimal_places' => $default?->decimal_places,
'native_currency_id' => (string)$this->native->id,
'native_currency_code' => $this->native->code,
'native_currency_symbol' => $this->native->symbol,
'native_currency_decimal_places' => $this->native->decimal_places,
'name' => $bill->name,
'amount_min' => app('steam')->bcround($bill->amount_min, $currency->decimal_places),
'amount_max' => app('steam')->bcround($bill->amount_max, $currency->decimal_places),
'native_amount_min' => $this->convertToNative ? app('steam')->bcround($bill->native_amount_min, $default->decimal_places) : null,
'native_amount_max' => $this->convertToNative ? app('steam')->bcround($bill->native_amount_max, $default->decimal_places) : null,
'amount_min' => $bill->amounts['amount_min'],
'amount_max' => $bill->amounts['amount_max'],
'amount_avg' => $bill->amounts['average'],
'date' => $bill->date->toAtomString(),
'end_date' => $bill->end_date?->toAtomString(),
'extension_date' => $bill->extension_date?->toAtomString(),
@@ -172,16 +158,16 @@ class BillTransformer extends AbstractTransformer
'skip' => $bill->skip,
'active' => $bill->active,
'order' => $bill->order,
'notes' => $notes,
'object_group_id' => null !== $objectGroupId ? (string)$objectGroupId : null,
'object_group_order' => $objectGroupOrder,
'object_group_title' => $objectGroupTitle,
'notes' => $bill->meta['notes'],
'object_group_id' => $bill->meta['object_group_id'],
'object_group_order' => $bill->meta['object_group_order'],
'object_group_title' => $bill->meta['object_group_title'],
// these fields need work:
'next_expected_match' => $nem,
'next_expected_match_diff' => $nemDiff,
'pay_dates' => $payDatesFormatted,
'paid_dates' => $paidDataFormatted,
// 'next_expected_match' => $nem,
// 'next_expected_match_diff' => $nemDiff,
// 'pay_dates' => $payDatesFormatted,
// 'paid_dates' => $paidDataFormatted,
'links' => [
[
'rel' => 'self',
@@ -235,14 +221,14 @@ class BillTransformer extends AbstractTransformer
$result = [];
foreach ($set as $entry) {
$array = [
'transaction_group_id' => (string)$entry->transaction_group_id,
'transaction_journal_id' => (string)$entry->id,
'date' => $entry->date->format('Y-m-d'),
'date_object' => $entry->date,
'currency_id' => $entry->transaction_currency_id,
'currency_code' => $entry->transaction_currency_code,
'currency_decimal_places' => $entry->transaction_currency_decimal_places,
'amount' => Steam::bcround($entry->amount, $entry->transaction_currency_decimal_places),
'transaction_group_id' => (string)$entry->transaction_group_id,
'transaction_journal_id' => (string)$entry->id,
'date' => $entry->date->format('Y-m-d'),
'date_object' => $entry->date,
'currency_id' => $entry->transaction_currency_id,
'currency_code' => $entry->transaction_currency_code,
'currency_decimal_places' => $entry->transaction_currency_decimal_places,
'amount' => Steam::bcround($entry->amount, $entry->transaction_currency_decimal_places),
];
if (null !== $entry->foreign_amount && null !== $entry->foreign_currency_code) {
$array['foreign_currency_id'] = $entry->foreign_currency_id;

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

48
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.21",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "473a8c30e450d87099f76313edcbb90852f9afdf"
"reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/473a8c30e450d87099f76313edcbb90852f9afdf",
"reference": "473a8c30e450d87099f76313edcbb90852f9afdf",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/1ccf445757458c06a04eb3f803603cb118fe5fa6",
"reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6",
"shasum": ""
},
"require": {
@@ -11119,7 +11119,7 @@
"type": "github"
}
],
"time": "2025-07-21T19:58:24+00:00"
"time": "2025-07-28T19:35:08+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -11218,16 +11218,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "12.3.1",
"version": "12.3.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "ddec29dfc128eba9c204389960f2063f3b7fa170"
"reference": "086553c5b2e0e1e20293d782d788ab768202b621"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ddec29dfc128eba9c204389960f2063f3b7fa170",
"reference": "ddec29dfc128eba9c204389960f2063f3b7fa170",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/086553c5b2e0e1e20293d782d788ab768202b621",
"reference": "086553c5b2e0e1e20293d782d788ab768202b621",
"shasum": ""
},
"require": {
@@ -11283,7 +11283,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.1"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.2"
},
"funding": [
{
@@ -11303,7 +11303,7 @@
"type": "tidelift"
}
],
"time": "2025-06-18T08:58:13+00:00"
"time": "2025-07-29T06:19:24+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -11552,16 +11552,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.2.7",
"version": "12.2.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "8b1348b254e5959acaf1539c6bd790515fb49414"
"reference": "49dc7c0669b97c7a36f63411635f48eedc140eb4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8b1348b254e5959acaf1539c6bd790515fb49414",
"reference": "8b1348b254e5959acaf1539c6bd790515fb49414",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49dc7c0669b97c7a36f63411635f48eedc140eb4",
"reference": "49dc7c0669b97c7a36f63411635f48eedc140eb4",
"shasum": ""
},
"require": {
@@ -11575,7 +11575,7 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
"phpunit/php-code-coverage": "^12.3.1",
"phpunit/php-code-coverage": "^12.3.2",
"phpunit/php-file-iterator": "^6.0.0",
"phpunit/php-invoker": "^6.0.0",
"phpunit/php-text-template": "^5.0.0",
@@ -11629,7 +11629,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.2.7"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.2.8"
},
"funding": [
{
@@ -11653,7 +11653,7 @@
"type": "tidelift"
}
],
"time": "2025-07-11T04:11:13+00:00"
"time": "2025-07-30T05:58:18+00:00"
},
{
"name": "rector/rector",

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-23',
'build_time' => 1753247028,
'version' => 'develop/2025-07-31',
'build_time' => 1753936691,
'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',

268
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": {
@@ -1699,6 +1699,13 @@
"node": ">=10.0.0"
}
},
"node_modules/@epic-web/invariant": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
"dev": true,
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
@@ -2142,9 +2149,9 @@
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
"integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.0.tgz",
"integrity": "sha512-X48nISrSOa89zu2VMljC4XaRf8NmgTwQBVHfS2Nu5G00ZwM31oOVrAtGxZF3b6wDYf9lJsf/Eq4cCSFKIkOWPQ==",
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
"engines": {
"node": ">=6"
@@ -2585,9 +2592,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz",
"integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==",
"cpu": [
"arm"
],
@@ -2599,9 +2606,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz",
"integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==",
"cpu": [
"arm64"
],
@@ -2613,9 +2620,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz",
"integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==",
"cpu": [
"arm64"
],
@@ -2627,9 +2634,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz",
"integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==",
"cpu": [
"x64"
],
@@ -2641,9 +2648,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz",
"integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==",
"cpu": [
"arm64"
],
@@ -2655,9 +2662,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz",
"integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==",
"cpu": [
"x64"
],
@@ -2669,9 +2676,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz",
"integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==",
"cpu": [
"arm"
],
@@ -2683,9 +2690,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz",
"integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==",
"cpu": [
"arm"
],
@@ -2697,9 +2704,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz",
"integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==",
"cpu": [
"arm64"
],
@@ -2711,9 +2718,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz",
"integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==",
"cpu": [
"arm64"
],
@@ -2725,9 +2732,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz",
"integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==",
"cpu": [
"loong64"
],
@@ -2738,10 +2745,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz",
"integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==",
"cpu": [
"ppc64"
],
@@ -2753,9 +2760,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz",
"integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==",
"cpu": [
"riscv64"
],
@@ -2767,9 +2774,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz",
"integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==",
"cpu": [
"riscv64"
],
@@ -2781,9 +2788,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz",
"integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==",
"cpu": [
"s390x"
],
@@ -2795,9 +2802,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz",
"integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==",
"cpu": [
"x64"
],
@@ -2809,9 +2816,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz",
"integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==",
"cpu": [
"x64"
],
@@ -2823,9 +2830,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz",
"integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==",
"cpu": [
"arm64"
],
@@ -2837,9 +2844,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz",
"integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==",
"cpu": [
"ia32"
],
@@ -2851,9 +2858,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.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz",
"integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==",
"cpu": [
"x64"
],
@@ -3931,14 +3938,14 @@
}
},
"node_modules/axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -4479,9 +4486,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"version": "1.0.30001731",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
"dev": true,
"funding": [
{
@@ -5018,22 +5025,21 @@
}
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.0.0.tgz",
"integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.1"
"@epic-web/invariant": "^1.0.0",
"cross-spawn": "^7.0.6"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
"cross-env": "dist/bin/cross-env.js",
"cross-env-shell": "dist/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
"node": ">=20"
}
},
"node_modules/cross-fetch": {
@@ -5694,9 +5700,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.192",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz",
"integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==",
"dev": true,
"license": "ISC"
},
@@ -7832,14 +7838,14 @@
}
},
"node_modules/launch-editor": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz",
"integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==",
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.0.tgz",
"integrity": "sha512-R/PIF14L6e2eHkhvQPu7jDRCr0msfCYCxbYiLgkkAGi0dVPWuM+RrsPu0a5dpuNe0KWGL3jpAkOlv53xGfPheQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"picocolors": "^1.0.0",
"shell-quote": "^1.8.1"
"picocolors": "^1.1.1",
"shell-quote": "^1.8.3"
}
},
"node_modules/leaflet": {
@@ -10117,9 +10123,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.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
"integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10133,26 +10139,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.2",
"@rollup/rollup-android-arm64": "4.46.2",
"@rollup/rollup-darwin-arm64": "4.46.2",
"@rollup/rollup-darwin-x64": "4.46.2",
"@rollup/rollup-freebsd-arm64": "4.46.2",
"@rollup/rollup-freebsd-x64": "4.46.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.46.2",
"@rollup/rollup-linux-arm-musleabihf": "4.46.2",
"@rollup/rollup-linux-arm64-gnu": "4.46.2",
"@rollup/rollup-linux-arm64-musl": "4.46.2",
"@rollup/rollup-linux-loongarch64-gnu": "4.46.2",
"@rollup/rollup-linux-ppc64-gnu": "4.46.2",
"@rollup/rollup-linux-riscv64-gnu": "4.46.2",
"@rollup/rollup-linux-riscv64-musl": "4.46.2",
"@rollup/rollup-linux-s390x-gnu": "4.46.2",
"@rollup/rollup-linux-x64-gnu": "4.46.2",
"@rollup/rollup-linux-x64-musl": "4.46.2",
"@rollup/rollup-win32-arm64-msvc": "4.46.2",
"@rollup/rollup-win32-ia32-msvc": "4.46.2",
"@rollup/rollup-win32-x64-msvc": "4.46.2",
"fsevents": "~2.3.2"
}
},
@@ -11518,15 +11524,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"
@@ -11838,9 +11844,9 @@
"license": "BSD-2-Clause"
},
"node_modules/webpack": {
"version": "5.100.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.100.2.tgz",
"integrity": "sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==",
"version": "5.101.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.0.tgz",
"integrity": "sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12502,9 +12508,9 @@
"devDependencies": {
"@johmun/vue-tags-input": "^2",
"@vue/compiler-sfc": "^3.5.11",
"axios": "^1.8",
"axios": "^1.11",
"bootstrap-sass": "^3",
"cross-env": "^7.0",
"cross-env": "^10.0",
"font-awesome": "^4.7.0",
"jquery": "^3",
"laravel-mix": "^6.0",
@@ -12519,7 +12525,7 @@
"resources/assets/v2": {
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.0",
"@fortawesome/fontawesome-free": "^7",
"@popperjs/core": "^2.11.8",
"admin-lte": "^4.0.0-rc4",
"alpinejs": "^3.13.7",

View File

@@ -16,9 +16,9 @@
"devDependencies": {
"@johmun/vue-tags-input": "^2",
"@vue/compiler-sfc": "^3.5.11",
"axios": "^1.8",
"axios": "^1.11",
"bootstrap-sass": "^3",
"cross-env": "^7.0",
"cross-env": "^10.0",
"font-awesome": "^4.7.0",
"jquery": "^3",
"laravel-mix": "^6.0",

View File

@@ -16,7 +16,7 @@
"vite-plugin-manifest-sri": "^0.2.0"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.0",
"@fortawesome/fontawesome-free": "^7",
"@popperjs/core": "^2.11.8",
"admin-lte": "^4.0.0-rc4",
"alpinejs": "^3.13.7",

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

@@ -315,7 +315,7 @@ let index = function () {
// need to find the input thing
console.log('Clicked edit button for account on index #' + index + ' and field ' + fieldName);
const querySelector = 'input[data-field="' + fieldName + '"][data-index="' + index + '"]';
console.log(querySelector);
// console.log(querySelector);
const newValue = document.querySelectorAll(querySelector)[0].value ?? '';
if ('' === newValue) {
return;

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;
@@ -201,7 +216,7 @@ export default () => ({
balance.amount_formatted = formatMoney(balance.amount, balance.currency_code);
return balance;
});
console.log(parent);
// console.log(parent);
// get groups for account:
@@ -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! (dashboard/boxes)');
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');
@@ -76,7 +86,7 @@ export default () => ({
continue;
}
let key = current.key;
console.log('NOT NATIVE');
// console.log('NOT NATIVE');
if (key.startsWith('balance-in-')) {
this.balanceBox.amounts.push(formatMoney(current.monetary_value, current.currency_code));
continue;
@@ -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

@@ -48,9 +48,19 @@ export default () => ({
}
this.getFreshData();
},
eventListeners: {
['@convert-to-native.window'](event){
console.log('I heard that! (dashboard/budgets)');
this.convertToNative = event.detail;
chartData = null;
this.loadChart();
}
},
drawChart(options) {
if (null !== chart) {
chart.data.datasets = options.data.datasets;
chart.data = options.data;
chart.update();
return;
}
@@ -59,8 +69,9 @@ export default () => ({
getFreshData() {
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 cacheKey = getCacheKey('ds_bdg_chart', {convertToNative: this.convertToNative, start: start, end: end});
//const cacheValid = window.store.get('cacheValid');
const cacheValid = false;
let cachedData = window.store.get(cacheKey);
if (cacheValid && typeof cachedData !== 'undefined') {
@@ -80,7 +91,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 +105,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 +114,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 +136,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 +154,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 +167,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 +184,7 @@ export default () => ({
this.loadChart();
}
});
window.store.observe('convertToNative', (newValue) => {
window.store.observe('convert_to_native', (newValue) => {
if (!afterPromises) {
return;
}

View File

@@ -33,6 +33,17 @@ let afterPromises = false;
export default () => ({
loading: false,
convertToNative: false,
eventListeners: {
['@convert-to-native.window'](event){
console.log('I heard that! (dashboard/categories)');
this.convertToNative = event.detail;
chartData = null;
this.loadChart();
}
},
generateOptions(data) {
currencies = [];
let options = getDefaultChartSettings('column');
@@ -43,11 +54,6 @@ export default () => ({
if (data.hasOwnProperty(i)) {
let current = data[i];
let code = current.currency_code;
// only use native code when doing auto conversion.
if (this.convertToNative) {
code = current.native_currency_code;
}
if (!series.hasOwnProperty(code)) {
series[code] = {
name: code,
@@ -65,9 +71,6 @@ export default () => ({
let yAxis = 'y';
let current = data[i];
let code = current.currency_code;
if (this.convertToNative) {
code = current.native_currency_code;
}
// loop series, add 0 if not present or add actual amount.
for (const ii in series) {
@@ -77,10 +80,6 @@ export default () => ({
// this series' currency matches this column's currency.
amount = parseFloat(current.amount);
yAxis = 'y' + current.currency_code;
if (this.convertToNative) {
amount = parseFloat(current.native_amount);
yAxis = 'y' + current.native_currency_code;
}
}
if (series[ii].data.hasOwnProperty(current.label)) {
// there is a value for this particular currency. The amount from this column will be added.
@@ -147,7 +146,7 @@ export default () => ({
getFreshData() {
const start = new Date(window.store.get('start'));
const end = new Date(window.store.get('end'));
const cacheKey = getCacheKey('ds_ct_chart', {start: start, end: end});
const cacheKey = getCacheKey('ds_ct_chart', {convertToNative: this.convertToNative, start: start, end: end});
const cacheValid = window.store.get('cacheValid');
let cachedData = window.store.get(cacheKey);
@@ -183,7 +182,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 +194,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,26 @@ Chart.register({
Legend
});
let index = function () {
return {
convertToNative: false,
saveNativeSettings(event) {
let target = event.currentTarget || event.target;
setVariable('convert_to_native',target.checked).then(() => {
console.log('Set convert to native to: ', target.checked);
this.$dispatch('convert-to-native', target.checked);
});
},
init() {
Promise.all([getVariable('convert_to_native', false)]).then((values) => {
this.convertToNative = values[0];
});
}
}
};
const comps = {
index,
dates,
boxes,
accounts,

View File

@@ -36,7 +36,7 @@ export default () => ({
const start = new Date(window.store.get('start'));
const end = new Date(window.store.get('end'));
// needs user data.
const cacheKey = getCacheKey(PIGGY_CACHE_KEY, {start: start, end: end});
const cacheKey = getCacheKey(PIGGY_CACHE_KEY, {convertToNative: this.convertToNative, start: start, end: end});
const cacheValid = window.store.get('cacheValid');
let cachedData = window.store.get(cacheKey);
@@ -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

@@ -29,17 +29,16 @@ import i18next from "i18next";
Chart.register({SankeyController, Flow});
const SANKEY_CACHE_KEY = 'ds_sankey_data';
let currencies = [];
let afterPromises = false;
let chart = null;
let transactions = [];
let convertToNative = false;
let translations = {
let currencies = [];
let afterPromises = false;
let chart = null;
let transactions = [];
let convertToNative = false;
let translations = {
category: null,
unknown_category: null,
in: null,
out: null,
// TODO
unknown_source: null,
unknown_dest: null,
unknown_account: null,
@@ -80,74 +79,97 @@ const getColor = function (key) {
// little helper
function getObjectName(type, name, direction, code) {
if(convertToNative) {
return getObjectNameWithoutCurrency(type, name, direction);
}
return getObjectNameWithCurrency(type, name, direction, code);
// category 4x
if ('category' === type && null !== name && 'in' === direction) {
return translations.category + ' "' + name + '" (' + translations.in + (convertToNative ? ', ' + code + ')' : ')');
}
if ('category' === type && null === name && 'in' === direction) {
return translations.unknown_category + ' (' + translations.in + (convertToNative ? ', ' + code + ')' : ')');
}
if ('category' === type && null !== name && 'out' === direction) {
return translations.category + ' "' + name + '" (' + translations.out + (convertToNative ? ', ' + 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 + ')' : '');
}
if ('account' === type && null !== name && 'in' === direction) {
return translations.revenue_account + '"' + name + '"' + (convertToNative ? ' (' + code + ')' : '');
}
if ('account' === type && null === name && 'out' === direction) {
return translations.unknown_dest + (convertToNative ? ' (' + code + ')' : '');
}
if ('account' === type && null !== name && 'out' === direction) {
return translations.expense_account + ' "' + name + '"' + (convertToNative ? ' (' + code + ')' : '');
}
// budget 2x
if ('budget' === type && null !== name) {
return translations.budget + ' "' + name + '"' + (convertToNative ? ' (' + code + ')' : '');
}
if ('budget' === type && null === name) {
return translations.unknown_budget + (convertToNative ? ' (' + code + ')' : '');
}
console.error('Cannot handle: type:"' + type + '", dir: "' + direction + '"');
}
function getLabelName(type, name, code) {
// category
if ('category' === type && null !== name) {
return translations.category + ' "' + name + '"' + (convertToNative ? ' (' + code + ')' : '');
function getObjectNameWithoutCurrency(type, name, direction) {
if('category' === type) {
let catName = null === name ? translations.unknown_category : translations.category + ' "' + name + '"';
let directionText = 'in' === direction ? translations.in : translations.out;
return catName + ' (' + directionText + ')';
}
if ('category' === type && null === name) {
return translations.unknown_category + (convertToNative ? ' (' + code + ')' : '');
if('account' === type) {
let accountName = null === name ? translations.unknown_account : name;
let directionText = 'in' === direction ? translations.in : translations.out;
let fullAccountName = 'in' === direction ? translations.revenue_account + ' "' + accountName + '"' : translations.expense_account + ' "' + accountName + '"';
return fullAccountName + ' (' + directionText + ')';
}
// account
if ('account' === type && null === name) {
return translations.unknown_account + (convertToNative ? ' (' + code + ')' : '');
if('budget' === type) {
return null === name ? translations.unknown_budget : translations.budget + ' "' + name + '"';
}
if ('account' === type && null !== name) {
return name + (convertToNative ? ' (' + code + ')' : '');
console.error('[a] Cannot handle: type:"' + type + '", dir: "' + direction + '"');
}
function getObjectNameWithCurrency(type, name, direction, code) {
if('category' === type) {
let catName = null === name ? translations.unknown_category : translations.category + ' "' + name + '"';
let directionText = 'in' === direction ? translations.in : translations.out;
return catName + ' (' + directionText + ', ' + code + ')';
}
// budget 2x
if ('budget' === type && null !== name) {
return translations.budget + ' "' + name + '"' + (convertToNative ? ' (' + code + ')' : '');
if('account' === type) {
let accountName = null === name ? translations.unknown_account : name;
let directionText = 'in' === direction ? translations.in : translations.out;
let fullAccountName = 'in' === direction ? translations.revenue_account + ' "' + accountName + '"' : translations.expense_account + ' "' + accountName + '"';
return fullAccountName + ' (' + directionText + ', ' + code + ')';
}
if ('budget' === type && null === name) {
return translations.unknown_budget + (convertToNative ? ' (' + code + ')' : '');
if('budget' === type) {
return (null === name ? translations.unknown_budget : translations.budget + ' "' + name + '"') + ' (' + code + ')';
}
console.error('Cannot handle: type:"' + type + '"');
console.error('[b] Cannot handle: type:"' + type + '", dir: "' + direction + '"');
}
function getLabel(type, name, code) {
if(convertToNative) {
return getLabelWithoutCurrency(type, name);
}
return getLabelWithCurrency(type, name, code);
}
function getLabelWithoutCurrency(type, name) {
if('category' === type) {
return null === name ? translations.unknown_category : translations.category + ' "' + name + '"';
}
if('account' === type) {
return null === name ? translations.unknown_account : name;
}
if('budget' === type) {
return null === name ? translations.unknown_budget : translations.budget + ' "' + name + '"';
}
console.error('[a] Cannot handle: type:"' + type + '"');
}
function getLabelWithCurrency(type, name, code) {
if('category' === type) {
return (null === name ? translations.unknown_category : translations.category + ' "' + name + '"') + ' ('+ code + ')';
}
if('account' === type) {
return (null === name ? translations.unknown_account : name) + ' (' + code + ')';
}
if('budget' === type) {
return (null === name ? translations.unknown_budget : translations.budget + ' "' + name + '"') + ' (' + code + ')';;
}
console.error('[b] Cannot handle: type:"' + type + '"');
}
export default () => ({
loading: false,
convertToNative: false,
processedData: null,
eventListeners: {
['@convert-to-native.window'](event){
console.log('I heard that! (dashboard/sankey)');
this.convertToNative = event.detail;
convertToNative = event.detail;
this.processedData = null;
this.loadChart();
}
},
generateOptions() {
let options = getDefaultChartSettings('sankey');
@@ -155,120 +177,22 @@ export default () => ({
currencies = [];
// variables collected for the sankey chart:
let amounts = {};
let labels = {};
for (let i in transactions) {
if (transactions.hasOwnProperty(i)) {
let group = transactions[i];
for (let ii in group.attributes.transactions) {
if (group.attributes.transactions.hasOwnProperty(ii)) {
// 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;
let amount = this.convertToNative ? parseFloat(transaction.native_amount) : parseFloat(transaction.amount);
let flowKey;
/*
Two entries in the sankey diagram for deposits:
1. From the revenue account (source) to a category (in).
2. From the category (in) to the big inbox.
*/
if ('deposit' === transaction.type) {
// nr 1
let category = getObjectName('category', transaction.category_name, 'in', currencyCode);
let revenueAccount = getObjectName('account', transaction.source_name, 'in', currencyCode);
labels[category] = getLabelName('category', transaction.category_name, currencyCode);
labels[revenueAccount] = getLabelName('account', transaction.source_name, currencyCode);
flowKey = revenueAccount + '-' + category + '-' + currencyCode;
if (!amounts.hasOwnProperty(flowKey)) {
amounts[flowKey] = {
from: revenueAccount,
to: category,
amount: 0
};
}
amounts[flowKey].amount += amount;
// nr 2
flowKey = category + '-' + translations.all_money + '-' + currencyCode;
if (!amounts.hasOwnProperty(flowKey)) {
amounts[flowKey] = {
from: category,
to: translations.all_money + (this.convertToNative ? ' (' + currencyCode + ')' : ''),
amount: 0
};
}
amounts[flowKey].amount += amount;
}
/*
Three entries in the sankey diagram for withdrawals:
1. From the big box to a budget.
2. From a budget to a category.
3. From a category to an expense account.
*/
if ('withdrawal' === transaction.type) {
// 1.
let budget = getObjectName('budget', transaction.budget_name, 'out', currencyCode);
labels[budget] = getLabelName('budget', transaction.budget_name, currencyCode);
flowKey = translations.all_money + '-' + budget + '-' + currencyCode;
if (!amounts.hasOwnProperty(flowKey)) {
amounts[flowKey] = {
from: translations.all_money + (this.convertToNative ? ' (' + currencyCode + ')' : ''),
to: budget,
amount: 0
};
}
amounts[flowKey].amount += amount;
// 2.
let category = getObjectName('category', transaction.category_name, 'out', currencyCode);
labels[category] = getLabelName('category', transaction.category_name, currencyCode);
flowKey = budget + '-' + category + '-' + currencyCode;
if (!amounts.hasOwnProperty(flowKey)) {
amounts[flowKey] = {
from: budget,
to: category,
amount: 0
};
}
amounts[flowKey].amount += amount;
// 3.
let expenseAccount = getObjectName('account', transaction.destination_name, 'out', currencyCode);
labels[expenseAccount] = getLabelName('account', transaction.destination_name, currencyCode);
flowKey = category + '-' + expenseAccount + '-' + currencyCode;
if (!amounts.hasOwnProperty(flowKey)) {
amounts[flowKey] = {
from: category,
to: expenseAccount,
amount: 0
};
}
amounts[flowKey].amount += amount;
}
}
}
}
}
this.parseTransactionGroups(transactions);
let dataSet =
// sankey chart has one data set.
{
label: 'Firefly III dashboard sankey chart',
data: [],
colorFrom: (c) => getColor(c.dataset.data[c.dataIndex] ? c.dataset.data[c.dataIndex].from : ''),
colorTo: (c) => getColor(c.dataset.data[c.dataIndex] ? c.dataset.data[c.dataIndex].to : ''),
colorMode: 'gradient', // or 'from' or 'to'
labels: labels,
size: 'min', // or 'min' if flow overlap is preferred
};
for (let i in amounts) {
if (amounts.hasOwnProperty(i)) {
let amount = amounts[i];
// sankey chart has one data set.
{
label: 'Firefly III dashboard sankey chart',
data: [],
colorFrom: (c) => getColor(c.dataset.data[c.dataIndex] ? c.dataset.data[c.dataIndex].from : ''),
colorTo: (c) => getColor(c.dataset.data[c.dataIndex] ? c.dataset.data[c.dataIndex].to : ''),
colorMode: 'gradient', // or 'from' or 'to'
labels: this.processedData.labels,
size: 'min', // or 'min' if flow overlap is preferred
};
for (let i in this.processedData.amounts) {
if (this.processedData.amounts.hasOwnProperty(i)) {
let amount = this.processedData.amounts[i];
dataSet.data.push({from: amount.from, to: amount.to, flow: amount.amount});
}
}
@@ -276,6 +200,133 @@ export default () => ({
return options;
},
parseTransactionGroups(groups) {
this.processedData = {
amounts: {},
labels: {}
};
for (let i in groups) {
if (groups.hasOwnProperty(i)) {
let group = groups[i];
this.parseTransactionGroup(group);
}
}
},
parseTransactionGroup(group) {
for (let ii in group.attributes.transactions) {
if (group.attributes.transactions.hasOwnProperty(ii)) {
// properties of the transaction, used in the generation of the chart:
let transaction = group.attributes.transactions[ii];
this.parseTransaction(transaction);
}
}
},
parseTransaction(transaction) {
let currencyCode = transaction.currency_code;
let amount = parseFloat(transaction.amount);
let flowKey;
if (this.convertToNative) {
currencyCode = transaction.native_currency_code;
amount = parseFloat(transaction.native_amount);
}
if ('deposit' === transaction.type) {
this.parseDeposit(transaction, currencyCode, amount);
return;
}
if ('withdrawal' === transaction.type) {
this.parseWithdrawal(transaction, currencyCode, amount);
}
},
parseWithdrawal(transaction, currencyCode, amount) {
/*
Three entries in the sankey diagram for withdrawals:
1. From the big box to a budget.
2. From a budget to a category.
3. From a category to an expense account.
*/
// first one:
let budget = getObjectName('budget', transaction.budget_name, 'out', currencyCode);
this.processedData.labels[budget] = getLabel('budget', transaction.budget_name, currencyCode);
let flowKey = translations.all_money + '-' + budget + '-' + currencyCode;
if (!this.processedData.amounts.hasOwnProperty(flowKey)) {
this.processedData.amounts[flowKey] = {
from: translations.all_money + (this.convertToNative ? ' (' + currencyCode + ')' : ''),
to: budget,
amount: 0
};
}
this.processedData.amounts[flowKey].amount += amount;
// second one:
let category = getObjectName('category', transaction.category_name, 'out', currencyCode);
this.processedData.labels[category] = getLabel('category', transaction.category_name, currencyCode);
flowKey = budget + '-' + category + '-' + currencyCode;
if (!this.processedData.amounts.hasOwnProperty(flowKey)) {
this.processedData.amounts[flowKey] = {
from: budget,
to: category,
amount: 0
};
}
this.processedData.amounts[flowKey].amount += amount;
// third one:
let expenseAccount = getObjectName('account', transaction.destination_name, 'out', currencyCode);
this.processedData.labels[expenseAccount] = getLabel('account', transaction.destination_name, currencyCode);
flowKey = category + '-' + expenseAccount + '-' + currencyCode;
if (!this.processedData.amounts.hasOwnProperty(flowKey)) {
this.processedData.amounts[flowKey] = {
from: category,
to: expenseAccount,
amount: 0
};
}
this.processedData.amounts[flowKey].amount += amount;
},
parseDeposit(transaction, currencyCode, amount) {
/*
Two entries in the sankey diagram for deposits:
1. From the revenue account (source) to a category (in).
2. From the category (in) to the big inbox.
*/
// this is the first one:
let category = getObjectName('category', transaction.category_name, 'in', currencyCode);
let revenueAccount = getObjectName('account', transaction.source_name, 'in', currencyCode);
let flowKey = revenueAccount + '-' + category + '-' + currencyCode;
this.processedData.labels[category] = getLabel('category', transaction.category_name, currencyCode);
this.processedData.labels[revenueAccount] = getLabel('account', transaction.source_name, currencyCode);
// create if necessary:
if (!this.processedData.amounts.hasOwnProperty(flowKey)) {
this.processedData.amounts[flowKey] = {
from: revenueAccount,
to: category,
amount: 0
};
}
this.processedData.amounts[flowKey].amount += amount;
// this is the second one:
flowKey = category + '-' + translations.all_money + '-' + currencyCode;
if (!this.processedData.amounts.hasOwnProperty(flowKey)) {
this.processedData.amounts[flowKey] = {
from: category,
to: translations.all_money + (this.convertToNative ? ' (' + currencyCode + ')' : ''),
amount: 0
};
}
this.processedData.amounts[flowKey].amount += amount;
},
drawChart(options) {
if (null !== chart) {
chart.data.datasets = options.data.datasets;
@@ -286,12 +337,12 @@ export default () => ({
},
getFreshData() {
const start = new Date(window.store.get('start'));
const end = new Date(window.store.get('end'));
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 cacheValid = window.store.get('cacheValid');
let cachedData = window.store.get(cacheKey);
let cachedData = window.store.get(cacheKey);
if (cacheValid && typeof cachedData !== 'undefined') {
transactions = cachedData;
@@ -310,9 +361,9 @@ export default () => ({
this.downloadTransactions(params);
},
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 start = new Date(window.store.get('start'));
const end = new Date(window.store.get('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,26 +399,27 @@ 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');
translations.in = i18next.t('firefly.money_flowing_in');
translations.out = i18next.t('firefly.money_flowing_out');
translations.unknown_category = i18next.t('firefly.unknown_category_plain');
translations.unknown_source = i18next.t('firefly.unknown_source_plain');
translations.unknown_dest = i18next.t('firefly.unknown_dest_plain');
translations.unknown_account = i18next.t('firefly.unknown_any_plain');
translations.unknown_budget = i18next.t('firefly.unknown_budget_plain');
translations.expense_account = i18next.t('firefly.expense_account');
translations.revenue_account = i18next.t('firefly.revenue_account');
translations.budget = i18next.t('firefly.budget');
convertToNative = values[0];
// console.log('sankey after promises');
afterPromises = true;
this.loadChart();
// some translations:
translations.all_money = i18next.t('firefly.all_money');
translations.category = i18next.t('firefly.category');
translations.in = i18next.t('firefly.money_flowing_in');
translations.out = i18next.t('firefly.money_flowing_out');
translations.unknown_category = i18next.t('firefly.unknown_category_plain');
translations.unknown_source = i18next.t('firefly.unknown_source_plain');
translations.unknown_dest = i18next.t('firefly.unknown_dest_plain');
translations.unknown_account = i18next.t('firefly.unknown_any_plain');
translations.unknown_budget = i18next.t('firefly.unknown_budget_plain');
translations.expense_account = i18next.t('firefly.expense_account');
translations.revenue_account = i18next.t('firefly.revenue_account');
translations.budget = i18next.t('firefly.budget');
// console.log('sankey after promises');
afterPromises = true;
this.loadChart();
});
window.store.observe('end', () => {
@@ -378,7 +430,7 @@ export default () => ({
this.transactions = [];
this.loadChart();
});
window.store.observe('convertToNative', (newValue) => {
window.store.observe('convert_to_native', (newValue) => {
if (!afterPromises) {
return;
}

View File

@@ -44,6 +44,18 @@ function addObjectGroupInfo(data) {
}
}
function parsePayDates(list) {
let newList = [];
for(let i in list) {
if (list.hasOwnProperty(i)) {
let current = list[i];
// convert to date object:
newList.push(new Date(current));
}
}
return newList;
}
function parseBillInfo(data) {
let result = {
id: data.id,
@@ -55,7 +67,7 @@ function parseBillInfo(data) {
// paid transactions:
transactions: [],
// unpaid moments
pay_dates: data.attributes.pay_dates,
pay_dates: parsePayDates(data.attributes.pay_dates),
paid: data.attributes.paid_dates.length > 0,
};
// set variables
@@ -64,6 +76,7 @@ function parseBillInfo(data) {
times: data.attributes.pay_dates.length,
amount: result.expected_amount
});
// console.log(result);
return result;
}
@@ -96,6 +109,21 @@ function parsePaidTransactions(paid_dates, bill) {
return result;
}
function isInRange(bill) {
let start = new Date(window.store.get('start'));
let end = new Date(window.store.get('end'));
for(let i in bill.pay_dates) {
if (bill.pay_dates.hasOwnProperty(i)) {
let currentDate = bill.pay_dates[i];
//console.log(currentDate);
if (currentDate >= start && currentDate <= end) {
return true;
}
}
}
return false;
}
function downloadSubscriptions(params) {
const getter = new Get();
return getter.list(params)
@@ -113,10 +141,18 @@ function downloadSubscriptions(params) {
// create and update the bill.
let bill = parseBillInfo(current);
// if not yet paid, and pay_dates is not in current rage, ignore it.
if (false === bill.paid && !isInRange(bill)) {
console.warn('Bill "'+bill.name+'" is not paid and not in range, ignoring: ');
continue;
}
bill.transactions = parsePaidTransactions(current.attributes.paid_dates, bill);
subscriptionData[objectGroupId].bills.push(bill);
if (0 === current.attributes.paid_dates.length) {
if (false === bill.paid) {
// bill is unpaid, count the "pay_dates" and multiply with the "amount".
// since bill is unpaid, this can only be in currency amount and native currency amount.
const totalAmount = current.attributes.pay_dates.length * bill.amount;
@@ -127,13 +163,10 @@ function downloadSubscriptions(params) {
currency_code: bill.currency_code,
paid: 0,
unpaid: 0,
native_currency_code: bill.native_currency_code,
native_paid: 0,
//native_unpaid: 0,
};
}
subscriptionData[objectGroupId].payment_info[bill.currency_code].unpaid += totalAmount;
//subscriptionData[objectGroupId].payment_info[bill.currency_code].native_unpaid += totalNativeAmount;
}
if (current.attributes.paid_dates.length > 0) {
@@ -141,8 +174,6 @@ function downloadSubscriptions(params) {
if (current.attributes.paid_dates.hasOwnProperty(ii)) {
// bill is paid!
// since bill is paid, 3 possible currencies:
// native, currency, foreign currency.
// foreign currency amount (converted to native or not) will be ignored.
let currentJournal = current.attributes.paid_dates[ii];
// new array for the currency
if (!subscriptionData[objectGroupId].payment_info.hasOwnProperty(currentJournal.currency_code)) {
@@ -150,15 +181,10 @@ function downloadSubscriptions(params) {
currency_code: bill.currency_code,
paid: 0,
unpaid: 0,
// native_currency_code: bill.native_currency_code,
// native_paid: 0,
//native_unpaid: 0,
};
}
const amount = parseFloat(currentJournal.amount) * -1;
// const nativeAmount = parseFloat(currentJournal.native_amount) * -1;
subscriptionData[objectGroupId].payment_info[currentJournal.currency_code].paid += amount;
// subscriptionData[objectGroupId].payment_info[currentJournal.currency_code].native_paid += nativeAmount;
}
}
}
@@ -181,6 +207,17 @@ export default () => ({
loading: false,
convertToNative: false,
subscriptions: [],
formatMoney(amount, currencyCode) {
return formatMoney(amount, currencyCode);
},
eventListeners: {
['@convert-to-native.window'](event){
console.log('I heard that! (dashboard/subscriptions)');
this.convertToNative = event.detail;
this.startSubscriptions();
}
},
startSubscriptions() {
this.loading = true;
let start = new Date(window.store.get('start'));
@@ -219,6 +256,7 @@ export default () => ({
//console.log(group);
}
}
console.log('Subscriptions: ', this.subscriptions);
// then assign to this.subscriptions.
this.loading = false;
@@ -268,7 +306,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;
@@ -286,7 +324,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

@@ -26,7 +26,28 @@ $danger: #CD5029 !default;
$primary: #1E6581 !default;
$success: #64B624 !default;
// admin LTE
@use "admin-lte/src/scss/adminlte" with (
$color-mode-type: $color-mode-type,
$link-decoration: $link-decoration,
$font-family-sans-serif: $font-family-sans-serif,
$danger: $danger,
$primary: $primary,
$success: $success
);
@use '@fortawesome/fontawesome-free/scss/variables' with (
$font-path: "@fortawesome/fontawesome-free/webfonts"
);
@use '@fortawesome/fontawesome-free/scss/fontawesome';
@use '@fortawesome/fontawesome-free/scss/fa' as fa;
@use '@fortawesome/fontawesome-free/scss/solid.scss' as fa-solid;
@use '@fortawesome/fontawesome-free/scss/brands.scss' as fa-brands;
@use '@fortawesome/fontawesome-free/scss/regular.scss' as fa-regular;
// some local CSS
.skip-links {display: none;}
/*
@@ -59,16 +80,7 @@ h3.hover-expand:hover {
appearance: auto;
}
// Bootstrap
// @import "bootstrap/scss/bootstrap";
// admin LTE
@import "adminlte-filtered";
// @import "~bootstrap-sass/assets/stylesheets/bootstrap";
// hover buttons
// edit buttons
.hidden-edit-button {
cursor: pointer;
}
@@ -76,12 +88,12 @@ td:not(:hover) .hidden-edit-button {
visibility: hidden;
}
// Bootstrap
// @import "bootstrap/scss/bootstrap";
// @import "~bootstrap-sass/assets/stylesheets/bootstrap";
// hover buttons
// Font awesome
//@import "~font-awesome/css/font-awesome";
$fa-font-path: "@fortawesome/fontawesome-free/webfonts";
@import "@fortawesome/fontawesome-free/scss/fontawesome.scss";
@import "@fortawesome/fontawesome-free/scss/solid.scss";
@import "@fortawesome/fontawesome-free/scss/brands.scss";
@import "@fortawesome/fontawesome-free/scss/regular.scss";

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,19 +28,25 @@ 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);
// post to user preferences (because why not):
let putter = new Put();
putter.put(name, value).then((response) => {
}).catch(() => {
return putter.put(name, value).then((response) => {
console.log('set "'+name+'" to value: ', value);
return Promise.resolve(response);
}).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);
return Promise.resolve(response);
});
});
}

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

@@ -234,6 +234,7 @@ return [
'advanced_options' => 'Advanced options',
'advanced_options_explain' => 'Some pages in Firefly III have advanced options hidden behind this button. This page doesn\'t have anything fancy here, but do check out the others!',
'here_be_dragons' => 'Hic sunt dracones',
'bad_date_transaction' => 'Firefly III has detected you have transactions from before the year 1970. Please correct these transactions at your earliest convenience.',
// Webhooks
'webhooks' => 'Webhooks',
@@ -1863,6 +1864,9 @@ return [
'remove_budgeted_amount' => 'Remove budgeted amount in :currency',
// bills:
'left_to_pay_lc' => 'left to pay',
'less_than_expected' => 'less than expected',
'more_than_expected' => 'more than expected',
'skip_help_text' => 'Use the skip field to create bi-monthly (skip = 1) or other custom intervals.',
'subscription' => 'Subscription',
'not_expected_period' => 'Not expected this period',

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

@@ -1,4 +1,4 @@
<div class="row mb-2" x-data="budgets">
<div class="row mb-2" x-data="budgets" x-bind="eventListeners">
<div class="col">
<div class="card">
<div class="card-header">

View File

@@ -1,4 +1,4 @@
<div class="row mb-2" x-data="categories">
<div class="row mb-2" x-data="categories" x-bind="eventListeners">
<div class="col">
<div class="card">
<div class="card-header">

View File

@@ -3,10 +3,8 @@
<template x-for="group in piggies">
<div class="card mb-2">
<div class="card-header">
<h3 class="card-title"><a href="{{ route('piggy-banks.index') }}"
title="{{ __('firefly.go_to_piggies') }}">{{ __('firefly.piggy_banks') }}
(<span
x-text="group.title"></span>)</a></h3>
<h3 class="card-title"><a href="{{ route('piggy-banks.index') }}" title="{{ __('firefly.go_to_piggies') }}">{{ __('firefly.piggy_banks') }}
(<span x-text="group.title"></span>)</a></h3>
</div>
<ul class="list-group list-group-flush">
<template x-for="piggy in group.piggies">

View File

@@ -6,7 +6,7 @@
>{{ __('firefly.income_and_expense') }}</a>
</h3>
</div>
<div class="card-body" x-data="sankey">
<div class="card-body" x-data="sankey" x-bind="eventListeners">
<canvas id="sankey-chart"></canvas>
</div>
</div>

View File

@@ -1,4 +1,4 @@
<div class="col" x-data="subscriptions">
<div class="col" x-data="subscriptions" x-bind="eventListeners">
<template x-for="group in subscriptions">
<div class="card mb-2">
<div class="card-header">
@@ -9,15 +9,21 @@
</h3>
</div>
<div class="card-body">
<template x-for="pi in group.payment_info">
<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'
:width="group.width"
x-init="drawPieChart(group.id, group.title, pie)"></canvas>
<div class="col">
<div class="progress" role="progressbar" aria-label="Example with label"
:aria-valuenow="(pi.paid*-1/pi.unpaid)*100" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar progress-bar-striped" :style="'width: ' + (pi.paid*-1/pi.unpaid)*100 + '%'">
<span x-text="formatMoney(pi.paid*-1,pi.currency_code)"></span>
</div>
</div>
</template>
<p>
<small>~ <span x-text="formatMoney(pi.unpaid, pi.currency_code)"></span> {{ __('firefly.left_to_pay_lc') }}</small>
</p>
</div>
</div>
</template>
<div class="row mb-2">
<table class="table table-striped table-hover">
<thead>
@@ -30,9 +36,7 @@
<template x-for="bill in group.bills">
<tr>
<td>
<a :href="'{{ route('subscriptions.show',['']) }}/' + bill.id" :title="bill.name">
<span x-text="bill.name"></span>
</a>
<a :href="'{{ route('subscriptions.show',['']) }}/' + bill.id" :title="bill.name"><span x-text="bill.name"></span></a>
<template x-if="bill.paid">
<small class="text-muted"><br>{{ __('firefly.paid') }}</small>
</template>
@@ -57,8 +61,17 @@
<ul class="list-unstyled">
<template x-for="transaction in bill.transactions">
<li>
<span x-text="transaction.amount"></span>
(<span x-text="transaction.percentage"></span>%)
<span :title="transaction.amount" x-text="transaction.amount"></span>
<template x-if="transaction.percentage < 0">
<span>
(<span :title="transaction.percentage + '% {{ __("firefly.less_than_expected") }}'" x-text="transaction.percentage"></span>%)
</span>
</template>
<template x-if="transaction.percentage > 0">
<span>
(<span :title="transaction.percentage + '% {{ __("firefly.more_than_expected") }}'" x-text="transaction.percentage"></span>%)
</span>
</template>
</li>
</template>
</ul>