diff --git a/.ci/php-cs-fixer/composer.lock b/.ci/php-cs-fixer/composer.lock index 04ea32ba02..40ac982c67 100644 --- a/.ci/php-cs-fixer/composer.lock +++ b/.ci/php-cs-fixer/composer.lock @@ -345,16 +345,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "8520451a140d3f46ac33042715115e290cf5785f" + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", - "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { @@ -364,10 +364,10 @@ "fidry/makefile": "^0.2.0", "fidry/php-cs-fixer-config": "^1.1.2", "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^8.5.31 || ^9.5.26", "webmozarts/strict-phpunit": "^7.5" }, @@ -394,7 +394,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, "funding": [ { @@ -402,20 +402,20 @@ "type": "github" } ], - "time": "2024-08-06T10:04:20+00:00" + "time": "2025-08-14T07:29:31+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.85.1", + "version": "v3.86.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "2fb6d7f6c3398dca5786a1635b27405d73a417ba" + "reference": "4a952bd19dc97879b0620f495552ef09b55f7d36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2fb6d7f6c3398dca5786a1635b27405d73a417ba", - "reference": "2fb6d7f6c3398dca5786a1635b27405d73a417ba", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4a952bd19dc97879b0620f495552ef09b55f7d36", + "reference": "4a952bd19dc97879b0620f495552ef09b55f7d36", "shasum": "" }, "require": { @@ -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.85.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.86.0" }, "funding": [ { @@ -507,7 +507,7 @@ "type": "github" } ], - "time": "2025-07-29T22:22:50+00:00" + "time": "2025-08-13T22:36:21+00:00" }, { "name": "psr/container", diff --git a/.github/release-notes/alpha.md b/.github/release-notes/alpha.md new file mode 100644 index 0000000000..a9dca0594a --- /dev/null +++ b/.github/release-notes/alpha.md @@ -0,0 +1,20 @@ +Welcome to release %version of Firefly III. This **alpha** release contains the latest fixes, translations and features. It is probably buggy and may not work as expected. You can download the release below, and adventurous Docker users can find this release under the `alpha` tag. + +:warning: Please be careful with this alpha release, as it may not work as expected. + +Alpha releases are created to test new features and fixes before they are included in a stable release. They are not recommended for production use. This release was created on %date and may contain unexpected bugs. Data loss is rare but possible. + +## Changelog (not final) + +%changelog + +## Installation and upgrade instructions + +* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/). +* Alternatively, read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/) + +The release files are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/). + +## Support Firefly III + +Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration. diff --git a/.github/release-notes/beta.md b/.github/release-notes/beta.md new file mode 100644 index 0000000000..300aceee44 --- /dev/null +++ b/.github/release-notes/beta.md @@ -0,0 +1,20 @@ +Welcome to release %version of Firefly III. This **beta** release contains the latest fixes, translations and features. It may be buggy, nor work as expected. You can download the release below, and adventurous Docker users can find this release under the `beta` tag. + +:warning: Please be careful with this beta release, as it may not work as expected. + +Alpha releases are created to test new features and fixes before they are included in a stable release. They are not recommended for production use. This release was created on %date and may contain unexpected bugs. Data loss is rare but possible. + +## Changelog (not final) + +%changelog + +## Installation and upgrade instructions + +* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/). +* Alternatively, read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/) + +The release files are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/). + +## Support Firefly III + +Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration. diff --git a/.github/release-notes/branch.md b/.github/release-notes/branch.md new file mode 100644 index 0000000000..f5fe6ac5a3 --- /dev/null +++ b/.github/release-notes/branch.md @@ -0,0 +1,20 @@ +Welcome to release %version of Firefly III. This branch-related release contains the latest fixes, translations and features. It is probably buggy and may not work as expected. You can download the release below, and adventurous Docker users can find this release under the `branch-*` tag. + +:warning: Please be careful with this branch release, as it may not work as expected. + +Branch releases are created to test large new features that are developed alongside the normal release flow. They are not recommended for production use. This release was created on %date and may contain unexpected bugs. Data loss is rare but possible. + +## Changelog + +There is no changelog for this release, as it is not final. However, [changelog.md](https://github.com/firefly-iii/firefly-iii/blob/develop/changelog.md) may already contain entries for the future release that this branch will be a part of. + +## Installation and upgrade instructions + +* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/). +* Alternatively, read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/) + +The release files are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/). + +## Support Firefly III + +Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration. diff --git a/.github/release-notes/develop.md b/.github/release-notes/develop.md new file mode 100644 index 0000000000..8b5271af05 --- /dev/null +++ b/.github/release-notes/develop.md @@ -0,0 +1,20 @@ +Welcome to the latest development release of Firefly III. This test release contains the absolute latest fixes, translations and features. It is probably buggy and may not work as expected. You can download the release below, and adventurous Docker users can find this release under the `develop` tag. + +:warning: Please be careful with this pre-release, as it may not work as expected. + +This release was created on %date and may contain unexpected bugs. Data loss is rare but possible. + +## Changelog + +The changelog for this release may not be up-to-date, so it is not included. However, [changelog.md](https://github.com/firefly-iii/firefly-iii/blob/develop/changelog.md) may already contain entries for the future release. + +## Installation and upgrade instructions + +* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/). +* Alternatively, read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/) + +The releases are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/). + +## Support Firefly III + +Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration. diff --git a/.github/release-notes/release.md b/.github/release-notes/release.md new file mode 100644 index 0000000000..c1442d5630 --- /dev/null +++ b/.github/release-notes/release.md @@ -0,0 +1,16 @@ +Welcome to release %version of Firefly III. It contains the latest fixes, translations and features. Docker users can find this release under the `latest` tag. + +## Changelog + +%changelog + +## Installation and upgrade instructions + +* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/). +* Alternatively, read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/) + +The releases are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/). + +## Support Firefly III + +Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration. diff --git a/app/Api/V1/Controllers/Chart/AccountController.php b/app/Api/V1/Controllers/Chart/AccountController.php index becc661d3e..7c762aece1 100644 --- a/app/Api/V1/Controllers/Chart/AccountController.php +++ b/app/Api/V1/Controllers/Chart/AccountController.php @@ -24,13 +24,11 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Chart; -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\Enums\UserRoleEnum; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Exceptions\ValidationException; use FireflyIII\Models\Account; use FireflyIII\Models\Preference; use FireflyIII\Models\TransactionCurrency; @@ -40,7 +38,6 @@ use FireflyIII\Support\Facades\Preferences; use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Api\ApiSupport; use FireflyIII\Support\Http\Api\CollectsAccountsFromFilter; -use FireflyIII\User; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Log; @@ -52,6 +49,8 @@ class AccountController extends Controller use ApiSupport; use CollectsAccountsFromFilter; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; + private ChartData $chartData; private AccountRepositoryInterface $repository; @@ -63,11 +62,11 @@ class AccountController extends Controller parent::__construct(); $this->middleware( function ($request, $next) { - /** @var User $user */ - $user = auth()->user(); $this->chartData = new ChartData(); $this->repository = app(AccountRepositoryInterface::class); - $this->repository->setUser($user); + + $userGroup = $this->validateUserGroup($request); + $this->repository->setUserGroup($userGroup); return $next($request); } @@ -75,11 +74,9 @@ class AccountController extends Controller } /** - * TODO fix documentation - * * @throws FireflyException */ - public function dashboard(ChartRequest $request): JsonResponse + public function overview(ChartRequest $request): JsonResponse { $queryParameters = $request->getParameters(); $accounts = $this->getAccountList($queryParameters); @@ -120,14 +117,20 @@ class AccountController extends Controller // the currency that belongs to the account. 'currency_id' => (string)$currency->id, + 'currency_name' => $currency->name, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, + // the primary currency + 'primary_currency_id' => (string)$this->primaryCurrency->id, + // the default currency of the user (could be the same!) 'date' => $params['start']->toAtomString(), - 'start' => $params['start']->toAtomString(), - 'end' => $params['end']->toAtomString(), + 'start_date' => $params['start']->toAtomString(), + 'end_date' => $params['end']->toAtomString(), + 'type' => 'line', + 'yAxisID' => 0, 'period' => '1D', 'entries' => [], ]; @@ -162,91 +165,6 @@ class AccountController extends Controller $this->chartData->add($currentSet); } - /** - * This endpoint is documented at: - * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/charts/getChartAccountOverview - * - * @throws ValidationException - */ - public function overview(DateRequest $request): JsonResponse - { - // parameters for chart: - $dates = $request->getAll(); - - - /** @var Carbon $start */ - $start = $dates['start']; - - /** @var Carbon $end */ - $end = $dates['end']; - - // set dates to end of day + start of day: - $start->startOfDay(); - $end->endOfDay(); - - $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->primaryCurrency; - $currentStart = clone $start; - $range = Steam::finalAccountBalanceInRange($account, $start, clone $end, $this->convertToPrimary); - $previous = array_values($range)[0]['balance']; - $pcPrevious = 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 "pc_entries" if convertToPrimary is true: - if ($this->convertToPrimary) { - $currentSet['pc_entries'] = []; - $currentSet['primary_currency_id'] = (string)$this->primaryCurrency->id; - $currentSet['primary_currency_code'] = $this->primaryCurrency->code; - $currentSet['primary_currency_symbol'] = $this->primaryCurrency->symbol; - $currentSet['primary_currency_decimal_places'] = $this->primaryCurrency->decimal_places; - $pcPrevious = array_values($range)[0]['pc_balance']; - - } - - // also get the primary balance if convertToPrimary 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 primary balance, if relevant: - $pcBalance = null; - if ($this->convertToPrimary) { - $pcBalance = array_key_exists($format, $range) ? $range[$format]['pc_balance'] : $pcPrevious; - $pcPrevious = $pcBalance; - $currentSet['pc_entries'][$label] = $pcBalance; - } - - $currentStart->addDay(); - - } - $chartData[] = $currentSet; - } - - return response()->json($chartData); - } - private function getFrontPageAccountIds(): array { $defaultSet = $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value])->pluck('id')->toArray(); diff --git a/app/Api/V1/Controllers/Chart/BalanceController.php b/app/Api/V1/Controllers/Chart/BalanceController.php index 9be3c42830..52354aa7a3 100644 --- a/app/Api/V1/Controllers/Chart/BalanceController.php +++ b/app/Api/V1/Controllers/Chart/BalanceController.php @@ -7,11 +7,11 @@ namespace FireflyIII\Api\V1\Controllers\Chart; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Chart\ChartRequest; use FireflyIII\Enums\TransactionTypeEnum; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Support\Chart\ChartData; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Http\Api\AccountBalanceGrouped; use FireflyIII\Support\Http\Api\CleansChartData; @@ -25,8 +25,9 @@ class BalanceController extends Controller { use CleansChartData; use CollectsAccountsFromFilter; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; - private ChartData $chartData; + private array $chartData; private GroupCollectorInterface $collector; private AccountRepositoryInterface $repository; @@ -42,7 +43,7 @@ class BalanceController extends Controller $userGroup = $this->validateUserGroup($request); $this->repository->setUserGroup($userGroup); $this->collector->setUserGroup($userGroup); - $this->chartData = new ChartData(); + $this->chartData = []; // $this->default = app('amount')->getPrimaryCurrency(); return $next($request); @@ -66,10 +67,6 @@ class BalanceController extends Controller $queryParameters = $request->getParameters(); $accounts = $this->getAccountList($queryParameters); - // prepare for currency conversion and data collection: - /** @var TransactionCurrency $primary */ - $primary = Amount::getPrimaryCurrency(); - // get journals for entire period: $this->collector->setRange($queryParameters['start'], $queryParameters['end']) @@ -81,7 +78,7 @@ class BalanceController extends Controller $object = new AccountBalanceGrouped(); $object->setPreferredRange($queryParameters['period']); - $object->setPrimary($primary); + $object->setPrimary($this->primaryCurrency); $object->setAccounts($accounts); $object->setJournals($journals); $object->setStart($queryParameters['start']); @@ -89,9 +86,10 @@ class BalanceController extends Controller $object->groupByCurrencyAndPeriod(); $data = $object->convertToChartData(); foreach ($data as $entry) { - $this->chartData->add($entry); + $this->chartData[] = $entry; } + $this->chartData = $this->clean($this->chartData); - return response()->json($this->chartData->render()); + return response()->json($this->chartData); } } diff --git a/app/Api/V1/Controllers/Chart/BudgetController.php b/app/Api/V1/Controllers/Chart/BudgetController.php index e58d657d5b..a053ba00b7 100644 --- a/app/Api/V1/Controllers/Chart/BudgetController.php +++ b/app/Api/V1/Controllers/Chart/BudgetController.php @@ -81,7 +81,7 @@ class BudgetController extends Controller * * @throws FireflyException */ - public function dashboard(DateRequest $request): JsonResponse + public function overview(DateRequest $request): JsonResponse { $params = $request->getAll(); @@ -110,10 +110,12 @@ 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 = []; - $spent = $this->opsRepository->listExpenses($start, $end, null, new Collection([$budget])); - $expenses = $this->processExpenses($budget->id, $spent, $start, $end); + $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); + $converter = new ExchangeRateConverter(); + $currencies = [$this->primaryCurrency->id => $this->primaryCurrency]; /** * @var int $currencyId @@ -121,7 +123,14 @@ class BudgetController extends Controller */ foreach ($expenses as $currencyId => $row) { // budgeted, left and overspent are now 0. - $limit = $this->filterLimit($currencyId, $limits); + $limit = $this->filterLimit($currencyId, $limits); + + // primary currency entries + $row['pc_budgeted'] = '0'; + $row['pc_spent'] = '0'; + $row['pc_left'] = '0'; + $row['pc_overspent'] = '0'; + if (null !== $limit) { $row['budgeted'] = $limit->amount; $row['left'] = bcsub($row['budgeted'], bcmul($row['spent'], '-1')); @@ -129,7 +138,22 @@ class BudgetController extends Controller $row['left'] = 1 === bccomp($row['left'], '0') ? $row['left'] : '0'; $row['overspent'] = 1 === bccomp($row['overspent'], '0') ? $row['overspent'] : '0'; } - $rows[] = $row; + + // convert data if necessary. + if (true === $this->convertToPrimary && $currencyId !== $this->primaryCurrency->id) { + $currencies[$currencyId] ??= TransactionCurrency::find($currencyId); + $row['pc_budgeted'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['budgeted']); + $row['pc_spent'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['spent']); + $row['pc_left'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['left']); + $row['pc_overspent'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['overspent']); + } + if (true === $this->convertToPrimary && $currencyId === $this->primaryCurrency->id) { + $row['pc_budgeted'] = $row['budgeted']; + $row['pc_spent'] = $row['spent']; + $row['pc_left'] = $row['left']; + $row['pc_overspent'] = $row['overspent']; + } + $rows[] = $row; } @@ -140,23 +164,39 @@ class BudgetController extends Controller // } // is always an array - $return = []; + $return = []; foreach ($rows as $row) { $current = [ - 'label' => $budget->name, - 'currency_id' => (string)$row['currency_id'], - 'currency_code' => $row['currency_code'], - 'currency_name' => $row['currency_name'], - 'currency_decimal_places' => $row['currency_decimal_places'], - 'period' => null, - 'start' => $row['start'], - 'end' => $row['end'], - 'entries' => [ + 'label' => $budget->name, + 'currency_id' => (string)$row['currency_id'], + 'currency_name' => $row['currency_name'], + 'currency_code' => $row['currency_code'], + 'currency_decimal_places' => $row['currency_decimal_places'], + + 'primary_currency_id' => (string)$this->primaryCurrency->id, + 'primary_currency_name' => $this->primaryCurrency->name, + 'primary_currency_code' => $this->primaryCurrency->code, + 'primary_currency_symbol' => $this->primaryCurrency->symbol, + 'primary_currency_decimal_places' => $this->primaryCurrency->decimal_places, + + 'period' => null, + 'date' => $row['start'], + 'start_date' => $row['start'], + 'end_date' => $row['end'], + 'yAxisID' => 0, + 'type' => 'bar', + 'entries' => [ 'budgeted' => $row['budgeted'], 'spent' => $row['spent'], 'left' => $row['left'], 'overspent' => $row['overspent'], ], + 'pc_entries' => [ + 'budgeted' => $row['pc_budgeted'], + 'spent' => '0', + 'left' => '0', + 'overspent' => '0', + ], ]; $return[] = $current; } diff --git a/app/Api/V1/Controllers/Chart/CategoryController.php b/app/Api/V1/Controllers/Chart/CategoryController.php index 94968d09cc..9073627218 100644 --- a/app/Api/V1/Controllers/Chart/CategoryController.php +++ b/app/Api/V1/Controllers/Chart/CategoryController.php @@ -34,6 +34,7 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Api\CleansChartData; use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; @@ -77,7 +78,7 @@ class CategoryController extends Controller * * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ - public function dashboard(DateRequest $request): JsonResponse + public function overview(DateRequest $request): JsonResponse { /** @var Carbon $start */ $start = $this->parameters->get('start'); @@ -100,52 +101,70 @@ class CategoryController extends Controller /** @var array $journal */ foreach ($journals as $journal) { // 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']); + $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 = Steam::positive($journal['amount']); + $pcAmount = null; // overrule if necessary: + if ($this->convertToPrimary && $journalCurrencyId === $this->primaryCurrency->id) { + $pcAmount = $amount; + } if ($this->convertToPrimary && $journalCurrencyId !== $this->primaryCurrency->id) { $currencyId = (int)$this->primaryCurrency->id; $currencyName = (string)$this->primaryCurrency->name; $currencyCode = (string)$this->primaryCurrency->code; $currencySymbol = (string)$this->primaryCurrency->symbol; $currencyDecimalPlaces = (int)$this->primaryCurrency->decimal_places; - $convertedAmount = $converter->convert($currency, $this->primaryCurrency, $journal['date'], $amount); - Log::debug(sprintf('Converted %s %s to %s %s', $journal['currency_code'], $amount, $this->primaryCurrency->code, $convertedAmount)); - $amount = $convertedAmount; + $pcAmount = $converter->convert($currency, $this->primaryCurrency, $journal['date'], $amount); + Log::debug(sprintf('Converted %s %s to %s %s', $journal['currency_code'], $amount, $this->primaryCurrency->code, $pcAmount)); } - $categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category'); - $key = sprintf('%s-%s', $categoryName, $currencyCode); + $categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category'); + $key = sprintf('%s-%s', $categoryName, $currencyCode); // create arrays $return[$key] ??= [ - 'label' => $categoryName, - '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(), - 'amount' => '0', + 'label' => $categoryName, + 'currency_id' => (string)$currencyId, + 'currency_name' => $currencyName, + 'currency_code' => $currencyCode, + 'currency_symbol' => $currencySymbol, + 'currency_decimal_places' => $currencyDecimalPlaces, + 'primary_currency_id' => (string)$this->primaryCurrency->id, + 'primary_currency_name' => (string)$this->primaryCurrency->name, + 'primary_currency_code' => (string)$this->primaryCurrency->code, + 'primary_currency_symbol' => (string)$this->primaryCurrency->symbol, + 'primary_currency_decimal_places' => (int)$this->primaryCurrency->decimal_places, + 'period' => null, + 'start_date' => $start->toAtomString(), + 'end_date' => $end->toAtomString(), + 'yAxisID' => 0, + 'type' => 'bar', + 'entries' => [ + 'spent' => '0', + ], + 'pc_entries' => [ + 'spent' => '0', + ], ]; // add monies - $return[$key]['amount'] = bcadd($return[$key]['amount'], (string)$amount); + $return[$key]['entries']['spent'] = bcadd($return[$key]['entries']['spent'], (string)$amount); + if (null !== $pcAmount) { + $return[$key]['pc_entries']['spent'] = bcadd($return[$key]['pc_entries']['spent'], (string)$pcAmount); + } } $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['entries']['spent'] < (float)$b['entries']['spent'] ? 1 : -1); return response()->json($this->clean($return)); } diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php index 30a3b79677..265a72b616 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php @@ -28,13 +28,11 @@ use Carbon\Carbon; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\DestroyRequest; use FireflyIII\Enums\UserRoleEnum; -use FireflyIII\Exceptions\ValidationException; use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use Illuminate\Http\JsonResponse; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class DestroyController extends Controller { @@ -59,23 +57,25 @@ class DestroyController extends Controller public function destroy(DestroyRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse { - $date = $request->getDate(); - if (!$date instanceof Carbon) { - throw new ValidationException('Date is required'); - } - $rate = $this->repository->getSpecificRateOnDate($from, $to, $date); - if (!$rate instanceof CurrencyExchangeRate) { - throw new NotFoundHttpException(); - } - $this->repository->deleteRate($rate); + $this->repository->deleteRates($from, $to); return response()->json([], 204); } - public function destroySingle(CurrencyExchangeRate $exchangeRate): JsonResponse + public function destroySingleById(CurrencyExchangeRate $exchangeRate): JsonResponse { $this->repository->deleteRate($exchangeRate); return response()->json([], 204); } + + public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse + { + $exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date); + if (null !== $exchangeRate) { + $this->repository->deleteRate($exchangeRate); + } + + return response()->json([], 204); + } } diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php index bea0bb1078..3b2f8c6e4d 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; +use Carbon\Carbon; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\CurrencyExchangeRate; @@ -33,6 +34,7 @@ use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Transformers\ExchangeRateTransformer; use Illuminate\Http\JsonResponse; use Illuminate\Pagination\LengthAwarePaginator; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class ShowController @@ -76,7 +78,7 @@ class ShowController extends Controller ; } - public function showSingle(CurrencyExchangeRate $exchangeRate): JsonResponse + public function showSingleById(CurrencyExchangeRate $exchangeRate): JsonResponse { $transformer = new ExchangeRateTransformer(); $transformer->setParameters($this->parameters); @@ -86,4 +88,20 @@ class ShowController extends Controller ->header('Content-Type', self::CONTENT_TYPE) ; } + + public function showSingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse + { + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); + + $exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date); + if (null === $exchangeRate) { + throw new NotFoundHttpException(); + } + + return response() + ->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } } diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php index 8353d637a2..0e0f0fa8a1 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php @@ -24,21 +24,27 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; +use Carbon\Carbon; +use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByCurrenciesRequest; +use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByDateRequest; +use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreRequest; use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\CurrencyExchangeRate; -use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreRequest; -use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Transformers\ExchangeRateTransformer; use Illuminate\Http\JsonResponse; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; class StoreController extends Controller { use ValidatesUserGroupTrait; - public const string RESOURCE_KEY = 'exchange-rates'; - protected array $acceptedRoles = [UserRoleEnum::OWNER]; + public const string RESOURCE_KEY = 'exchange-rates'; + protected array $acceptedRoles = [UserRoleEnum::OWNER]; private ExchangeRateRepositoryInterface $repository; public function __construct() @@ -54,6 +60,71 @@ class StoreController extends Controller ); } + public function storeByCurrencies(StoreByCurrenciesRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse + { + + $data = $request->getAll(); + $collection = new Collection(); + + foreach ($data as $date => $rate) { + $date = Carbon::createFromFormat('Y-m-d', $date); + $existing = $this->repository->getSpecificRateOnDate($from, $to, $date); + if (null !== $existing) { + // update existing rate. + $existing = $this->repository->updateExchangeRate($existing, $rate); + $collection->push($existing); + + continue; + } + $new = $this->repository->storeExchangeRate($from, $to, $rate, $date); + $collection->push($new); + } + + $count = $collection->count(); + $paginator = new LengthAwarePaginator($collection, $count, $count, 1); + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); // give params to transformer + + return response() + ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } + + public function storeByDate(StoreByDateRequest $request, Carbon $date): JsonResponse + { + + $data = $request->getAll(); + $from = $request->getFromCurrency(); + $collection = new Collection(); + foreach ($data['rates'] as $key => $rate) { + $to = TransactionCurrency::where('code', $key)->first(); + if (null === $to) { + continue; // should not happen. + } + $existing = $this->repository->getSpecificRateOnDate($from, $to, $date); + if (null !== $existing) { + // update existing rate. + $existing = $this->repository->updateExchangeRate($existing, $rate); + $collection->push($existing); + + continue; + } + $new = $this->repository->storeExchangeRate($from, $to, $rate, $date); + $collection->push($new); + } + + $count = $collection->count(); + $paginator = new LengthAwarePaginator($collection, $count, $count, 1); + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); // give params to transformer + + return response() + ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } + public function store(StoreRequest $request): JsonResponse { $date = $request->getDate(); diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php index 788faf874d..8844407f7d 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php @@ -24,21 +24,24 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; -use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\UpdateRequest; +use Carbon\Carbon; use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\UpdateRequest; use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\CurrencyExchangeRate; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Transformers\ExchangeRateTransformer; use Illuminate\Http\JsonResponse; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class UpdateController extends Controller { use ValidatesUserGroupTrait; - public const string RESOURCE_KEY = 'exchange-rates'; - protected array $acceptedRoles = [UserRoleEnum::OWNER]; + public const string RESOURCE_KEY = 'exchange-rates'; + protected array $acceptedRoles = [UserRoleEnum::OWNER]; private ExchangeRateRepositoryInterface $repository; public function __construct() @@ -54,7 +57,7 @@ class UpdateController extends Controller ); } - public function update(UpdateRequest $request, CurrencyExchangeRate $exchangeRate): JsonResponse + public function updateById(UpdateRequest $request, CurrencyExchangeRate $exchangeRate): JsonResponse { $date = $request->getDate(); $rate = $request->getRate(); @@ -67,4 +70,23 @@ class UpdateController extends Controller ->header('Content-Type', self::CONTENT_TYPE) ; } + + public function updateByDate(UpdateRequest $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse + { + $exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date); + if (null === $exchangeRate) { + throw new NotFoundHttpException(); + } + $date = $request->getDate(); + $rate = $request->getRate(); + $exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date); + + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); + + return response() + ->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } } diff --git a/app/Api/V1/Requests/Models/CurrencyExchangeRate/DestroyRequest.php b/app/Api/V1/Requests/Models/CurrencyExchangeRate/DestroyRequest.php index 1762d7348c..82ff8ca3de 100644 --- a/app/Api/V1/Requests/Models/CurrencyExchangeRate/DestroyRequest.php +++ b/app/Api/V1/Requests/Models/CurrencyExchangeRate/DestroyRequest.php @@ -45,7 +45,7 @@ class DestroyRequest extends FormRequest public function rules(): array { return [ - 'date' => 'required|date|after:1970-01-02|before:2038-01-17', + // 'date' => 'required|date|after:1970-01-02|before:2038-01-17', ]; } } diff --git a/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php new file mode 100644 index 0000000000..67706bdc69 --- /dev/null +++ b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php @@ -0,0 +1,76 @@ +all(); + } + + /** + * The rules that the incoming request must be matched against. + */ + public function rules(): array + { + return [ + '*' => 'required|numeric|min:0.0000000001', + ]; + } + + public function withValidator(Validator $validator): void + { + $validator->after( + static function (Validator $validator): void { + $data = $validator->getData() ?? []; + foreach ($data as $date => $rate) { + try { + $date = Carbon::createFromFormat('Y-m-d', $date); + } catch (InvalidFormatException $e) { + $validator->errors()->add('date', trans('validation.date', ['attribute' => 'date'])); + + return; + } + if (!is_numeric($rate)) { + $validator->errors()->add('rate', trans('validation.number', ['attribute' => 'rate'])); + + return; + } + } + } + ); + } +} diff --git a/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php new file mode 100644 index 0000000000..433e6ac90b --- /dev/null +++ b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php @@ -0,0 +1,90 @@ + $this->get('from'), + 'rates' => $this->get('rates', []), + ]; + } + + public function getFromCurrency(): TransactionCurrency + { + return TransactionCurrency::where('code', $this->get('from'))->first(); + } + + /** + * The rules that the incoming request must be matched against. + */ + public function rules(): array + { + return [ + 'from' => 'required|exists:transaction_currencies,code', + 'rates' => 'required|array', + 'rates.*' => 'required|numeric|min:0.0000000001', + ]; + } + + public function withValidator(Validator $validator): void + { + $from = $this->getFromCurrency(); + + $validator->after( + static function (Validator $validator) use ($from): void { + $data = $validator->getData(); + $rates = $data['rates'] ?? []; + if (0 === count($rates)) { + $validator->errors()->add('rates', 'No rates given.'); + + return; + } + foreach ($rates as $key => $entry) { + if ($key === $from->code) { + $validator->errors()->add(sprintf('rates.%s', $key), trans('validation.convert_to_itself', ['code' => $key])); + + continue; + } + $to = TransactionCurrency::where('code', $key)->first(); + if (null === $to) { + $validator->errors()->add(sprintf('rates.%s', $key), trans('validation.invalid_currency_code', ['code' => $key])); + } + } + } + ); + } +} diff --git a/app/Api/V1/Requests/Models/CurrencyExchangeRate/UpdateRequest.php b/app/Api/V1/Requests/Models/CurrencyExchangeRate/UpdateRequest.php index ab37c34114..8114b7abed 100644 --- a/app/Api/V1/Requests/Models/CurrencyExchangeRate/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/CurrencyExchangeRate/UpdateRequest.php @@ -52,6 +52,8 @@ class UpdateRequest extends FormRequest return [ 'date' => 'date|after:1970-01-02|before:2038-01-17', 'rate' => 'required|numeric|gt:0', + 'from' => 'nullable|exists:transaction_currencies,code', + 'to' => 'nullable|exists:transaction_currencies,code', ]; } } diff --git a/app/Repositories/ExchangeRate/ExchangeRateRepository.php b/app/Repositories/ExchangeRate/ExchangeRateRepository.php index f7936e0979..1fecb036fd 100644 --- a/app/Repositories/ExchangeRate/ExchangeRateRepository.php +++ b/app/Repositories/ExchangeRate/ExchangeRateRepository.php @@ -112,4 +112,13 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface, UserGro return $object; } + + public function deleteRates(TransactionCurrency $from, TransactionCurrency $to): void + { + $this->userGroup->currencyExchangeRates() + ->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ->delete() + ; + } } diff --git a/app/Repositories/ExchangeRate/ExchangeRateRepositoryInterface.php b/app/Repositories/ExchangeRate/ExchangeRateRepositoryInterface.php index 4d2d024e01..04f8ac1072 100644 --- a/app/Repositories/ExchangeRate/ExchangeRateRepositoryInterface.php +++ b/app/Repositories/ExchangeRate/ExchangeRateRepositoryInterface.php @@ -47,6 +47,8 @@ interface ExchangeRateRepositoryInterface { public function deleteRate(CurrencyExchangeRate $rate): void; + public function deleteRates(TransactionCurrency $from, TransactionCurrency $to): void; + public function getAll(): Collection; public function getRates(TransactionCurrency $from, TransactionCurrency $to): Collection; diff --git a/app/Support/Chart/ChartData.php b/app/Support/Chart/ChartData.php index 1e04e0a654..ab03e3dd31 100644 --- a/app/Support/Chart/ChartData.php +++ b/app/Support/Chart/ChartData.php @@ -26,6 +26,9 @@ namespace FireflyIII\Support\Chart; use FireflyIII\Exceptions\FireflyException; +/** + * @deprecated + */ class ChartData { private array $series; diff --git a/app/Support/Http/Api/AccountBalanceGrouped.php b/app/Support/Http/Api/AccountBalanceGrouped.php index e3d84f3896..397146be61 100644 --- a/app/Support/Http/Api/AccountBalanceGrouped.php +++ b/app/Support/Http/Api/AccountBalanceGrouped.php @@ -28,6 +28,8 @@ use Carbon\Carbon; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Support\Facades\Navigation; +use FireflyIII\Support\Facades\Steam; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; @@ -66,34 +68,38 @@ class AccountBalanceGrouped // income and expense array prepped: $income = [ 'label' => 'earned', - 'currency_id' => (string) $currency['currency_id'], + 'currency_id' => (string)$currency['currency_id'], 'currency_symbol' => $currency['currency_symbol'], 'currency_code' => $currency['currency_code'], 'currency_decimal_places' => $currency['currency_decimal_places'], - 'primary_currency_id' => (string) $currency['primary_currency_id'], + 'primary_currency_id' => (string)$currency['primary_currency_id'], 'primary_currency_symbol' => $currency['primary_currency_symbol'], 'primary_currency_code' => $currency['primary_currency_code'], 'primary_currency_decimal_places' => $currency['primary_currency_decimal_places'], 'date' => $this->start->toAtomString(), - 'start' => $this->start->toAtomString(), - 'end' => $this->end->toAtomString(), + 'start_date' => $this->start->toAtomString(), + 'end_date' => $this->end->toAtomString(), + 'yAxisID' => 0, + 'type' => 'line', 'period' => $this->preferredRange, 'entries' => [], - 'primary_entries' => [], + 'pc_entries' => [], ]; $expense = [ 'label' => 'spent', - 'currency_id' => (string) $currency['currency_id'], + 'currency_id' => (string)$currency['currency_id'], 'currency_symbol' => $currency['currency_symbol'], 'currency_code' => $currency['currency_code'], 'currency_decimal_places' => $currency['currency_decimal_places'], - 'primary_currency_id' => (string) $currency['primary_currency_id'], + 'primary_currency_id' => (string)$currency['primary_currency_id'], 'primary_currency_symbol' => $currency['primary_currency_symbol'], 'primary_currency_code' => $currency['primary_currency_code'], 'primary_currency_decimal_places' => $currency['primary_currency_decimal_places'], 'date' => $this->start->toAtomString(), - 'start' => $this->start->toAtomString(), - 'end' => $this->end->toAtomString(), + 'start_date' => $this->start->toAtomString(), + 'end_date' => $this->end->toAtomString(), + 'type' => 'line', + 'yAxisID' => 0, 'period' => $this->preferredRange, 'entries' => [], 'pc_entries' => [], @@ -104,15 +110,15 @@ class AccountBalanceGrouped $key = $currentStart->format($this->carbonFormat); $label = $currentStart->toAtomString(); // normal entries - $income['entries'][$label] = app('steam')->bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']); - $expense['entries'][$label] = app('steam')->bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']); + $income['entries'][$label] = Steam::bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']); + $expense['entries'][$label] = Steam::bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']); // converted entries - $income['pc_entries'][$label] = app('steam')->bcround($currency[$key]['pc_earned'] ?? '0', $currency['primary_currency_decimal_places']); - $expense['pc_entries'][$label] = app('steam')->bcround($currency[$key]['pc_spent'] ?? '0', $currency['primary_currency_decimal_places']); + $income['pc_entries'][$label] = Steam::bcround($currency[$key]['pc_earned'] ?? '0', $currency['primary_currency_decimal_places']); + $expense['pc_entries'][$label] = Steam::bcround($currency[$key]['pc_spent'] ?? '0', $currency['primary_currency_decimal_places']); // next loop - $currentStart = app('navigation')->addPeriod($currentStart, $this->preferredRange, 0); + $currentStart = Navigation::addPeriod($currentStart, $this->preferredRange, 0); } $chartData[] = $income; @@ -143,7 +149,7 @@ class AccountBalanceGrouped { // format the date according to the period $period = $journal['date']->format($this->carbonFormat); - $currencyId = (int) $journal['currency_id']; + $currencyId = (int)$journal['currency_id']; $currency = $this->findCurrency($currencyId); // set the array with monetary info, if it does not exist. @@ -153,24 +159,24 @@ class AccountBalanceGrouped // is this journal's amount in- our outgoing? $key = $this->getDataKey($journal); - $amount = 'spent' === $key ? app('steam')->negative($journal['amount']) : app('steam')->positive($journal['amount']); + $amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']); // get conversion rate $rate = $this->getRate($currency, $journal['date']); - $amountConverted = bcmul((string) $amount, $rate); + $amountConverted = bcmul((string)$amount, $rate); // perhaps transaction already has the foreign amount in the primary currency. - if ((int) $journal['foreign_currency_id'] === $this->primary->id) { + if ((int)$journal['foreign_currency_id'] === $this->primary->id) { $amountConverted = $journal['foreign_amount'] ?? '0'; - $amountConverted = 'earned' === $key ? app('steam')->positive($amountConverted) : app('steam')->negative($amountConverted); + $amountConverted = 'earned' === $key ? Steam::positive($amountConverted) : Steam::negative($amountConverted); } // add normal entry - $this->data[$currencyId][$period][$key] = bcadd((string) $this->data[$currencyId][$period][$key], (string) $amount); + $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], (string)$amount); // add converted entry $convertedKey = sprintf('pc_%s', $key); - $this->data[$currencyId][$period][$convertedKey] = bcadd((string) $this->data[$currencyId][$period][$convertedKey], (string) $amountConverted); + $this->data[$currencyId][$period][$convertedKey] = bcadd((string)$this->data[$currencyId][$period][$convertedKey], (string)$amountConverted); } private function findCurrency(int $currencyId): TransactionCurrency @@ -185,15 +191,15 @@ class AccountBalanceGrouped private function createDefaultDataEntry(array $journal): void { - $currencyId = (int) $journal['currency_id']; + $currencyId = (int)$journal['currency_id']; $this->data[$currencyId] ??= [ - 'currency_id' => (string) $currencyId, + 'currency_id' => (string)$currencyId, 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code'], 'currency_name' => $journal['currency_name'], 'currency_decimal_places' => $journal['currency_decimal_places'], // primary currency info (could be the same) - 'primary_currency_id' => (string) $this->primary->id, + 'primary_currency_id' => (string)$this->primary->id, 'primary_currency_code' => $this->primary->code, 'primary_currency_symbol' => $this->primary->symbol, 'primary_currency_decimal_places' => $this->primary->decimal_places, @@ -202,14 +208,14 @@ class AccountBalanceGrouped private function createDefaultPeriodEntry(array $journal): void { - $currencyId = (int) $journal['currency_id']; + $currencyId = (int)$journal['currency_id']; $period = $journal['date']->format($this->carbonFormat); $this->data[$currencyId][$period] ??= [ - 'period' => $period, - 'spent' => '0', - 'earned' => '0', - 'pc_spent' => '0', - 'pc_earned' => '0', + 'period' => $period, + 'spent' => '0', + 'earned' => '0', + 'pc_spent' => '0', + 'pc_earned' => '0', ]; } @@ -258,12 +264,12 @@ class AccountBalanceGrouped $primaryCurrencyId = $primary->id; $this->currencies = [$primary->id => $primary]; // currency cache $this->data[$primaryCurrencyId] = [ - 'currency_id' => (string) $primaryCurrencyId, + 'currency_id' => (string)$primaryCurrencyId, 'currency_symbol' => $primary->symbol, 'currency_code' => $primary->code, 'currency_name' => $primary->name, 'currency_decimal_places' => $primary->decimal_places, - 'primary_currency_id' => (string) $primaryCurrencyId, + 'primary_currency_id' => (string)$primaryCurrencyId, 'primary_currency_symbol' => $primary->symbol, 'primary_currency_code' => $primary->code, 'primary_currency_name' => $primary->name, @@ -284,7 +290,7 @@ class AccountBalanceGrouped public function setPreferredRange(string $preferredRange): void { $this->preferredRange = $preferredRange; - $this->carbonFormat = app('navigation')->preferredCarbonFormatByPeriod($preferredRange); + $this->carbonFormat = Navigation::preferredCarbonFormatByPeriod($preferredRange); } public function setStart(Carbon $start): void diff --git a/app/Support/Http/Api/CleansChartData.php b/app/Support/Http/Api/CleansChartData.php index 46a6a2e4ab..4b02726c1b 100644 --- a/app/Support/Http/Api/CleansChartData.php +++ b/app/Support/Http/Api/CleansChartData.php @@ -47,24 +47,31 @@ trait CleansChartData * @var array $array */ foreach ($data as $index => $array) { - if (array_key_exists('currency_id', $array)) { - $array['currency_id'] = (string) $array['currency_id']; - } - if (array_key_exists('primary_currency_id', $array)) { - $array['primary_currency_id'] = (string) $array['primary_currency_id']; - } - if (!array_key_exists('start', $array)) { - throw new FireflyException(sprintf('Data-set "%s" is missing the "start"-variable.', $index)); - } - if (!array_key_exists('end', $array)) { - throw new FireflyException(sprintf('Data-set "%s" is missing the "end"-variable.', $index)); - } - if (!array_key_exists('period', $array)) { - throw new FireflyException(sprintf('Data-set "%s" is missing the "period"-variable.', $index)); - } + $array = $this->cleanSingleArray($index, $array); $return[] = $array; } return $return; } + + private function cleanSingleArray(mixed $index, array $array): array + { + if (array_key_exists('currency_id', $array)) { + $array['currency_id'] = (string)$array['currency_id']; + } + if (array_key_exists('primary_currency_id', $array)) { + $array['primary_currency_id'] = (string)$array['primary_currency_id']; + } + $required = [ + 'start_date', 'end_date', 'period', 'yAxisID', 'type', 'entries', 'pc_entries', + 'currency_id', 'primary_currency_id', + ]; + foreach ($required as $field) { + if (!array_key_exists($field, $array)) { + throw new FireflyException(sprintf('Data-set "%s" is missing the "%s"-variable.', $index, $field)); + } + } + + return $array; + } } diff --git a/app/Support/Http/Api/CollectsAccountsFromFilter.php b/app/Support/Http/Api/CollectsAccountsFromFilter.php index db304c5e7e..7c10a6c1e1 100644 --- a/app/Support/Http/Api/CollectsAccountsFromFilter.php +++ b/app/Support/Http/Api/CollectsAccountsFromFilter.php @@ -67,7 +67,7 @@ trait CollectsAccountsFromFilter if ('all' === $queryParameters['preselected']) { return $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]); } - if ('assets' === $queryParameters['preselected']) { + if ('assets' === $queryParameters['preselected'] || 'Asset account' === $queryParameters['preselected']) { return $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]); } if ('liabilities' === $queryParameters['preselected']) { diff --git a/app/Transformers/ExchangeRateTransformer.php b/app/Transformers/ExchangeRateTransformer.php index 6303fb0184..18566cc69a 100644 --- a/app/Transformers/ExchangeRateTransformer.php +++ b/app/Transformers/ExchangeRateTransformer.php @@ -48,11 +48,13 @@ class ExchangeRateTransformer extends AbstractTransformer 'updated_at' => $rate->updated_at->toAtomString(), 'from_currency_id' => (string) $rate->fromCurrency->id, + 'from_currency_name' => $rate->fromCurrency->name, 'from_currency_code' => $rate->fromCurrency->code, 'from_currency_symbol' => $rate->fromCurrency->symbol, 'from_currency_decimal_places' => $rate->fromCurrency->decimal_places, 'to_currency_id' => (string) $rate->toCurrency->id, + 'to_currency_name' => $rate->toCurrency->name, 'to_currency_code' => $rate->toCurrency->code, 'to_currency_symbol' => $rate->toCurrency->symbol, 'to_currency_decimal_places' => $rate->toCurrency->decimal_places, diff --git a/composer.lock b/composer.lock index d601034330..a6297fc8ce 100644 --- a/composer.lock +++ b/composer.lock @@ -1878,16 +1878,16 @@ }, { "name": "laravel/framework", - "version": "v12.23.1", + "version": "v12.24.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "2a0e9331a0db904236143fe915c281ff4be274a3" + "reference": "6dcf2c46da23d159f35d6246234953a74b740d83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/2a0e9331a0db904236143fe915c281ff4be274a3", - "reference": "2a0e9331a0db904236143fe915c281ff4be274a3", + "url": "https://api.github.com/repos/laravel/framework/zipball/6dcf2c46da23d159f35d6246234953a74b740d83", + "reference": "6dcf2c46da23d159f35d6246234953a74b740d83", "shasum": "" }, "require": { @@ -1997,7 +1997,7 @@ "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^10.0.0", + "orchestra/testbench-core": "^10.6.0", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", @@ -2091,7 +2091,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-08-12T17:35:05+00:00" + "time": "2025-08-13T20:30:36+00:00" }, { "name": "laravel/passport", @@ -10987,16 +10987,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.0", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", - "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "shasum": "" }, "require": { @@ -11015,7 +11015,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -11039,9 +11039,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" }, - "time": "2025-07-27T20:03:57+00:00" + "time": "2025-08-13T20:13:15+00:00" }, { "name": "phar-io/manifest", @@ -11876,16 +11876,16 @@ }, { "name": "rector/rector", - "version": "2.1.2", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "40a71441dd73fa150a66102f5ca1364c44fc8fff" + "reference": "dd430c869fddf4965049c8fd6f5ee49f155cfddf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/40a71441dd73fa150a66102f5ca1364c44fc8fff", - "reference": "40a71441dd73fa150a66102f5ca1364c44fc8fff", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/dd430c869fddf4965049c8fd6f5ee49f155cfddf", + "reference": "dd430c869fddf4965049c8fd6f5ee49f155cfddf", "shasum": "" }, "require": { @@ -11924,7 +11924,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.1.2" + "source": "https://github.com/rectorphp/rector/tree/2.1.3" }, "funding": [ { @@ -11932,7 +11932,7 @@ "type": "github" } ], - "time": "2025-07-17T19:30:06+00:00" + "time": "2025-08-13T11:43:04+00:00" }, { "name": "sebastian/cli-parser", @@ -12598,16 +12598,16 @@ }, { "name": "sebastian/recursion-context", - "version": "7.0.0", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c" + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c405ae3a63e01b32eb71577f8ec1604e39858a7c", - "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { @@ -12650,15 +12650,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2025-02-07T05:00:01+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { "name": "sebastian/type", diff --git a/config/firefly.php b/config/firefly.php index a4af3951a7..998250e370 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => '6.3.0-beta.1', - 'build_time' => 1755023090, + 'version' => '6.3.0-beta.2', + 'build_time' => 1755258109, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 26, diff --git a/package-lock.json b/package-lock.json index 3d52360e60..336c6d0d6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,22 +53,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -94,14 +94,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -151,18 +151,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", - "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.27.1", + "@babel/traverse": "^7.28.3", "semver": "^6.3.1" }, "engines": { @@ -266,15 +266,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -387,24 +387,24 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", - "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "dev": true, "license": "MIT", "dependencies": { @@ -416,13 +416,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -499,14 +499,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", - "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -726,13 +726,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", - "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -743,9 +743,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", - "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", + "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", "dev": true, "license": "MIT", "dependencies": { @@ -754,7 +754,7 @@ "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -1284,9 +1284,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz", - "integrity": "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz", + "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==", "dev": true, "license": "MIT", "dependencies": { @@ -1333,9 +1333,9 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.0.tgz", - "integrity": "sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.3.tgz", + "integrity": "sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==", "dev": true, "license": "MIT", "dependencies": { @@ -1512,9 +1512,9 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz", - "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", + "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", "dev": true, "license": "MIT", "dependencies": { @@ -1526,7 +1526,7 @@ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", @@ -1537,8 +1537,8 @@ "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.28.0", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.3", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-dotall-regex": "^7.27.1", @@ -1570,7 +1570,7 @@ "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.0", + "@babel/plugin-transform-regenerator": "^7.28.3", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1622,9 +1622,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1646,18 +1646,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.2", "debug": "^4.3.1" }, "engines": { @@ -1707,9 +1707,9 @@ "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", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -1724,9 +1724,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -1741,9 +1741,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -1758,9 +1758,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -1775,9 +1775,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -1792,9 +1792,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -1809,9 +1809,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -1826,9 +1826,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -1843,9 +1843,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -1860,9 +1860,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -1877,9 +1877,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -1894,9 +1894,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -1911,9 +1911,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -1928,9 +1928,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -1945,9 +1945,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -1962,9 +1962,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -1979,9 +1979,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -1996,9 +1996,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -2013,9 +2013,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -2030,9 +2030,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -2047,9 +2047,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -2064,9 +2064,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ "arm64" ], @@ -2081,9 +2081,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -2098,9 +2098,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -2115,9 +2115,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -2132,9 +2132,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -3148,9 +3148,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", - "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", "dependencies": { @@ -4486,9 +4486,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001734", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", - "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", + "version": "1.0.30001735", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", "dev": true, "funding": [ { @@ -5700,9 +5700,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.200", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz", - "integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==", + "version": "1.5.202", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.202.tgz", + "integrity": "sha512-NxbYjRmiHcHXV1Ws3fWUW+SLb62isauajk45LUJ/HgIOkUA7jLZu/X2Iif+X9FBNK8QkF9Zb4Q2mcwXCcY30mg==", "dev": true, "license": "ISC" }, @@ -5863,9 +5863,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5876,32 +5876,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { @@ -7052,9 +7052,9 @@ } }, "node_modules/i18next": { - "version": "25.3.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.4.tgz", - "integrity": "sha512-AHklEYFLiRRxW1Cb6zE9lfnEtYvsydRC8nRS3RSKGX3zCqZ8nLZwMaUsrb80YuccPNv2RNokDL8LkTNnp+6mDw==", + "version": "25.3.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.6.tgz", + "integrity": "sha512-dThZ0CTCM3sUG/qS0ZtQYZQcUI6DtBN8yBHK+SKEqihPcEYmjVWh/YJ4luic73Iq6Uxhp6q7LJJntRK5+1t7jQ==", "funding": [ { "type": "individual", @@ -11170,11 +11170,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -11617,11 +11620,14 @@ "license": "MIT" }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -11844,9 +11850,9 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.101.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.1.tgz", - "integrity": "sha512-rHY3vHXRbkSfhG6fH8zYQdth/BtDgXXuR2pHF++1f/EBkI8zkgM5XWfsC3BvOoW9pr1CvZ1qQCxhCEsbNgT50g==", + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.2.tgz", + "integrity": "sha512-4JLXU0tD6OZNVqlwzm3HGEhAHufSiyv+skb7q0d2367VDMzrU1Q/ZeepvkcHH0rZie6uqEtTQQe0OEOOluH3Mg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/readme.md b/readme.md index 5075f4400a..25a7d39e24 100644 --- a/readme.md +++ b/readme.md @@ -173,8 +173,6 @@ Over time, [many people have contributed to Firefly III](https://github.com/fire The Firefly III logo is made by the excellent Cherie Woo. -Featured|HelloGitHub - [packagist-shield]: https://img.shields.io/packagist/v/grumpydictator/firefly-iii.svg?style=flat-square [packagist-url]: https://packagist.org/packages/grumpydictator/firefly-iii [license-shield]: https://img.shields.io/github/license/firefly-iii/firefly-iii.svg?style=flat-square diff --git a/resources/assets/v1/src/components/exchange-rates/Rates.vue b/resources/assets/v1/src/components/exchange-rates/Rates.vue index 2ac2a6ef25..0ac13a94ad 100644 --- a/resources/assets/v1/src/components/exchange-rates/Rates.vue +++ b/resources/assets/v1/src/components/exchange-rates/Rates.vue @@ -284,9 +284,9 @@ export default { // console.log(parts); // delete A to B - axios.delete("./api/v1/exchange-rates/rates/" + parts.from + '/' + parts.to + '?date=' + format(parts.date, 'yyyy-MM-dd')); + axios.delete("./api/v1/exchange-rates/" + parts.from + '/' + parts.to + '/' + format(parts.date, 'yyyy-MM-dd')); // delete B to A. - axios.delete("./api/v1/exchange-rates/rates/" + parts.to + '/' + parts.from + '?date=' + format(parts.date, 'yyyy-MM-dd')); + axios.delete("./api/v1/exchange-rates/" + parts.to + '/' + parts.from + '/' + format(parts.date, 'yyyy-MM-dd')); this.rates.splice(index, 1); }, @@ -327,7 +327,7 @@ export default { downloadRates: function (page) { this.tempRates = {}; this.loading = true; - axios.get("./api/v1/exchange-rates/rates/" + this.from_code + '/' + this.to_code + '?page=' + page).then((response) => { + axios.get("./api/v1/exchange-rates/" + this.from_code + '/' + this.to_code + '?page=' + page).then((response) => { for (let i in response.data.data) { if (response.data.data.hasOwnProperty(i)) { let current = response.data.data[i]; diff --git a/resources/assets/v1/src/locales/de.json b/resources/assets/v1/src/locales/de.json index b15143e386..5b0607c38e 100644 --- a/resources/assets/v1/src/locales/de.json +++ b/resources/assets/v1/src/locales/de.json @@ -102,7 +102,7 @@ "profile_oauth_client_secret_title": "Client Secret", "profile_oauth_client_secret_expl": "Hier ist Ihr neuer pers\u00f6nlicher Zugangsschl\u00fcssel. Dies ist das einzige Mal, dass er angezeigt wird, also verlieren Sie ihn nicht! Sie k\u00f6nnen diesen Token jetzt verwenden, um API-Anfragen zu stellen.", "profile_oauth_confidential": "Vertraulich", - "profile_oauth_confidential_help": "Require the client to authenticate with a secret. Confidential clients can hold credentials in a secure way without exposing them to unauthorized parties. Public applications, such as native desktop or JavaScript SPA applications, are unable to hold secrets securely.", + "profile_oauth_confidential_help": "Verlangen Sie vom Client, sich mit einem Geheimnis zu authentifizieren. Vertrauliche Clients k\u00f6nnen Anmeldedaten auf sichere Weise speichern, ohne sie Unbefugten zug\u00e4nglich zu machen. \u00d6ffentliche Anwendungen, wie native Desktop- oder JavaScript-SPA-Anwendungen, sind nicht f\u00e4hig, Geheimnisse sicher zu speichern.", "multi_account_warning_unknown": "Abh\u00e4ngig von der Art der Buchung, die Sie anlegen, kann das Quell- und\/oder Zielkonto nachfolgender Aufteilungen durch das \u00fcberschrieben werden, was in der ersten Aufteilung der Buchung definiert wurde.", "multi_account_warning_withdrawal": "Bedenken Sie, dass das Quellkonto nachfolgender Aufteilungen von dem, was in der ersten Aufteilung der Abhebung definiert ist, au\u00dfer Kraft gesetzt wird.", "multi_account_warning_deposit": "Bedenken Sie, dass das Zielkonto nachfolgender Aufteilungen von dem, was in der ersten Aufteilung der Einnahmen definiert ist, au\u00dfer Kraft gesetzt wird.", diff --git a/resources/assets/v1/src/locales/fr.json b/resources/assets/v1/src/locales/fr.json index 462e02a54e..630079dacb 100644 --- a/resources/assets/v1/src/locales/fr.json +++ b/resources/assets/v1/src/locales/fr.json @@ -102,7 +102,7 @@ "profile_oauth_client_secret_title": "Secret du client", "profile_oauth_client_secret_expl": "Voici votre nouveau secret de client. C'est la seule fois qu'il sera affich\u00e9, donc ne le perdez pas ! Vous pouvez maintenant utiliser ce secret pour faire des requ\u00eates d'API.", "profile_oauth_confidential": "Confidentiel", - "profile_oauth_confidential_help": "Require the client to authenticate with a secret. Confidential clients can hold credentials in a secure way without exposing them to unauthorized parties. Public applications, such as native desktop or JavaScript SPA applications, are unable to hold secrets securely.", + "profile_oauth_confidential_help": "Exiger que le client s'authentifie avec un secret. Les clients confidentiels peuvent d\u00e9tenir des informations d'identification de mani\u00e8re s\u00e9curis\u00e9e sans les exposer \u00e0 des tiers non autoris\u00e9s. Les applications publiques, telles que les applications de bureau natif ou les SPA JavaScript, ne peuvent pas tenir des secrets en toute s\u00e9curit\u00e9.", "multi_account_warning_unknown": "Selon le type d'op\u00e9ration que vous cr\u00e9ez, le(s) compte(s) source et\/ou de destination des s\u00e9parations suivantes peuvent \u00eatre remplac\u00e9s par celui de la premi\u00e8re s\u00e9paration de l'op\u00e9ration.", "multi_account_warning_withdrawal": "Gardez en t\u00eate que le compte source des s\u00e9parations suivantes peut \u00eatre remplac\u00e9 par celui de la premi\u00e8re s\u00e9paration de la d\u00e9pense.", "multi_account_warning_deposit": "Gardez en t\u00eate que le compte de destination des s\u00e9parations suivantes peut \u00eatre remplac\u00e9 par celui de la premi\u00e8re s\u00e9paration du d\u00e9p\u00f4t.", diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 6794b8b8f7..3dd5cdd9b7 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -21,174 +21,173 @@ */ - declare(strict_types=1); return [ - 'invalid_account_type' => 'A piggy bank can only be linked to asset accounts and liabilities', - 'invalid_account_currency' => 'This account does not use the currency you have selected', - 'current_amount_too_much' => 'The combined amount in "current_amount" cannot exceed the "target_amount".', - 'filter_must_be_in' => 'Filter ":filter" must be one of: :values', - 'filter_not_string' => 'Filter ":filter" is expected to be a string of text', - 'bad_api_filter' => 'This API endpoint does not support ":filter" as a filter.', - 'nog_logged_in' => 'You are not logged in.', - 'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.', - 'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.', - 'missing_where' => 'Array is missing "where"-clause', - 'missing_update' => 'Array is missing "update"-clause', - 'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause', - 'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause', - 'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.', - 'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.', - 'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.', - 'iban' => 'This is not a valid IBAN.', - 'zero_or_more' => 'The value cannot be negative.', - 'more_than_zero' => 'The value must be more than zero.', - 'more_than_zero_correct' => 'The value must be zero or more.', - 'no_asset_account' => 'This is not an asset account.', - 'date_or_time' => 'The value must be a valid date or time value (ISO 8601).', - 'source_equals_destination' => 'The source account equals the destination account.', - 'unique_account_number_for_user' => 'It looks like this account number is already in use.', - 'unique_user_group_for_user' => 'It looks like this administration title is already in use.', - 'unique_iban_for_user' => 'It looks like this IBAN is already in use.', - 'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"', - 'deleted_user' => 'Due to security constraints, you cannot register using this email address.', - 'rule_trigger_value' => 'This value is invalid for the selected trigger.', - 'rule_action_expression' => 'Invalid expression. :error', - 'rule_action_value' => 'This value is invalid for the selected action.', - 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', - 'file_attached' => 'Successfully uploaded file ":name".', - 'file_zero' => 'The file is zero bytes in size.', - 'must_exist' => 'The ID in field :attribute does not exist in the database.', - 'all_accounts_equal' => 'All accounts in this field must be equal.', - 'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.', - 'transaction_types_equal' => 'All splits must be of the same type.', - 'invalid_transaction_type' => 'Invalid transaction type.', - 'invalid_selection' => 'Your selection is invalid.', - 'belongs_user' => 'This value is linked to an object that does not seem to exist.', - 'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.', - 'no_access_group' => 'The user has no access to this administration.', - 'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.', - 'at_least_one_transaction' => 'Need at least one transaction.', - 'recurring_transaction_id' => 'Need at least one transaction.', - 'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.', - 'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.', - 'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.', - 'at_least_one_repetition' => 'Need at least one repetition.', - 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', - 'require_currency_info' => 'The content of this field is invalid without currency information.', - 'require_currency_id_code' => 'Please set either "transaction_currency_id" or "transaction_currency_code".', - 'not_transfer_account' => 'This account is not an account that can be used for transfers.', - 'require_currency_amount' => 'The content of this field is invalid without foreign amount information.', - 'require_foreign_currency' => 'This field requires a number', - 'require_foreign_dest' => 'This field value must match the currency of the destination account.', - 'require_foreign_src' => 'This field value must match the currency of the source account.', - 'equal_description' => 'Transaction description should not equal global description.', - 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', - 'file_too_large' => 'File ":name" is too large.', - 'belongs_to_user' => 'The value of :attribute is unknown.', - 'accepted' => 'The :attribute must be accepted.', - 'bic' => 'This is not a valid BIC.', - 'at_least_one_trigger' => 'Rule must have at least one trigger.', - 'at_least_one_active_trigger' => 'Rule must have at least one active trigger.', - 'at_least_one_action' => 'Rule must have at least one action.', - 'at_least_one_active_action' => 'Rule must have at least one active action.', - 'base64' => 'This is not valid base64 encoded data.', - 'model_id_invalid' => 'The given ID seems invalid for this model.', - 'less' => ':attribute must be less than 10,000,000', - 'active_url' => 'The :attribute is not a valid URL.', - 'after' => 'The :attribute must be a date after :date.', - 'date_after' => 'The start date must be before the end date.', - 'alpha' => 'The :attribute may only contain letters.', - 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', - 'alpha_num' => 'The :attribute may only contain letters and numbers.', - 'array' => 'The :attribute must be an array.', - 'unique_for_user' => 'There already is an entry with this :attribute.', - 'before' => 'The :attribute must be a date before :date.', - 'unique_object_for_user' => 'This name is already in use.', - 'unique_account_for_user' => 'This account name is already in use.', + 'invalid_account_type' => 'A piggy bank can only be linked to asset accounts and liabilities', + 'invalid_account_currency' => 'This account does not use the currency you have selected', + 'current_amount_too_much' => 'The combined amount in "current_amount" cannot exceed the "target_amount".', + 'filter_must_be_in' => 'Filter ":filter" must be one of: :values', + 'filter_not_string' => 'Filter ":filter" is expected to be a string of text', + 'bad_api_filter' => 'This API endpoint does not support ":filter" as a filter.', + 'nog_logged_in' => 'You are not logged in.', + 'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.', + 'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.', + 'missing_where' => 'Array is missing "where"-clause', + 'missing_update' => 'Array is missing "update"-clause', + 'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause', + 'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause', + 'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.', + 'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.', + 'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.', + 'iban' => 'This is not a valid IBAN.', + 'zero_or_more' => 'The value cannot be negative.', + 'more_than_zero' => 'The value must be more than zero.', + 'more_than_zero_correct' => 'The value must be zero or more.', + 'no_asset_account' => 'This is not an asset account.', + 'date_or_time' => 'The value must be a valid date or time value (ISO 8601).', + 'source_equals_destination' => 'The source account equals the destination account.', + 'unique_account_number_for_user' => 'It looks like this account number is already in use.', + 'unique_user_group_for_user' => 'It looks like this administration title is already in use.', + 'unique_iban_for_user' => 'It looks like this IBAN is already in use.', + 'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"', + 'deleted_user' => 'Due to security constraints, you cannot register using this email address.', + 'rule_trigger_value' => 'This value is invalid for the selected trigger.', + 'rule_action_expression' => 'Invalid expression. :error', + 'rule_action_value' => 'This value is invalid for the selected action.', + 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', + 'file_attached' => 'Successfully uploaded file ":name".', + 'file_zero' => 'The file is zero bytes in size.', + 'must_exist' => 'The ID in field :attribute does not exist in the database.', + 'all_accounts_equal' => 'All accounts in this field must be equal.', + 'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.', + 'transaction_types_equal' => 'All splits must be of the same type.', + 'invalid_transaction_type' => 'Invalid transaction type.', + 'invalid_selection' => 'Your selection is invalid.', + 'belongs_user' => 'This value is linked to an object that does not seem to exist.', + 'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.', + 'no_access_group' => 'The user has no access to this administration.', + 'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.', + 'at_least_one_transaction' => 'Need at least one transaction.', + 'recurring_transaction_id' => 'Need at least one transaction.', + 'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.', + 'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.', + 'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.', + 'at_least_one_repetition' => 'Need at least one repetition.', + 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', + 'require_currency_info' => 'The content of this field is invalid without currency information.', + 'require_currency_id_code' => 'Please set either "transaction_currency_id" or "transaction_currency_code".', + 'not_transfer_account' => 'This account is not an account that can be used for transfers.', + 'require_currency_amount' => 'The content of this field is invalid without foreign amount information.', + 'require_foreign_currency' => 'This field requires a number', + 'require_foreign_dest' => 'This field value must match the currency of the destination account.', + 'require_foreign_src' => 'This field value must match the currency of the source account.', + 'equal_description' => 'Transaction description should not equal global description.', + 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', + 'file_too_large' => 'File ":name" is too large.', + 'belongs_to_user' => 'The value of :attribute is unknown.', + 'accepted' => 'The :attribute must be accepted.', + 'bic' => 'This is not a valid BIC.', + 'at_least_one_trigger' => 'Rule must have at least one trigger.', + 'at_least_one_active_trigger' => 'Rule must have at least one active trigger.', + 'at_least_one_action' => 'Rule must have at least one action.', + 'at_least_one_active_action' => 'Rule must have at least one active action.', + 'base64' => 'This is not valid base64 encoded data.', + 'model_id_invalid' => 'The given ID seems invalid for this model.', + 'less' => ':attribute must be less than 10,000,000', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'date_after' => 'The start date must be before the end date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'unique_for_user' => 'There already is an entry with this :attribute.', + 'before' => 'The :attribute must be a date before :date.', + 'unique_object_for_user' => 'This name is already in use.', + 'unique_account_for_user' => 'This account name is already in use.', - - 'between.numeric' => 'The :attribute must be between :min and :max.', - 'between.file' => 'The :attribute must be between :min and :max kilobytes.', - 'between.string' => 'The :attribute must be between :min and :max characters.', - 'between.array' => 'The :attribute must have between :min and :max items.', - 'boolean' => 'The :attribute field must be true or false.', - 'confirmed' => 'The :attribute confirmation does not match.', - 'date' => 'The :attribute is not a valid date.', - 'date_format' => 'The :attribute does not match the format :format.', - 'different' => 'The :attribute and :other must be different.', - 'digits' => 'The :attribute must be :digits digits.', - 'digits_between' => 'The :attribute must be between :min and :max digits.', - 'email' => 'The :attribute must be a valid email address.', - 'filled' => 'The :attribute field is required.', - 'exists' => 'The selected :attribute is invalid.', - 'image' => 'The :attribute must be an image.', - 'in' => 'The selected :attribute is invalid.', - 'integer' => 'The :attribute must be an integer.', - 'ip' => 'The :attribute must be a valid IP address.', - 'json' => 'The :attribute must be a valid JSON string.', - 'max.numeric' => 'The :attribute may not be greater than :max.', - 'max.file' => 'The :attribute may not be greater than :max kilobytes.', - 'max.string' => 'The :attribute may not be greater than :max characters.', - 'max.array' => 'The :attribute may not have more than :max items.', - 'mimes' => 'The :attribute must be a file of type: :values.', - 'min.numeric' => 'The :attribute must be at least :min.', - 'lte.numeric' => 'The :attribute must be less than or equal :value.', - 'min.file' => 'The :attribute must be at least :min kilobytes.', - 'min.string' => 'The :attribute must be at least :min characters.', - 'min.array' => 'The :attribute must have at least :min items.', - 'not_in' => 'The selected :attribute is invalid.', - 'numeric' => 'The :attribute must be a number.', - 'scientific_notation' => 'The :attribute cannot use the scientific notation.', - 'numeric_primary' => 'The primary currency amount must be a number.', - 'numeric_destination' => 'The destination amount must be a number.', - 'numeric_source' => 'The source amount must be a number.', - 'generic_invalid' => 'This value is invalid.', - 'transaction_type_changed' => 'If you change the type of the transaction, make sure the correct source/destination accounts are set.', - 'regex' => 'The :attribute format is invalid.', - 'required' => 'The :attribute field is required.', - 'required_if' => 'The :attribute field is required when :other is :value.', - 'required_unless' => 'The :attribute field is required unless :other is in :values.', - 'required_with' => 'The :attribute field is required when :values is present.', - 'required_with_all' => 'The :attribute field is required when :values is present.', - 'required_without' => 'The :attribute field is required when :values is not present.', - 'required_without_all' => 'The :attribute field is required when none of :values are present.', - 'same' => 'The :attribute and :other must match.', - 'size.numeric' => 'The :attribute must be :size.', - 'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.', - 'size.file' => 'The :attribute must be :size kilobytes.', - 'size.string' => 'The :attribute must be :size characters.', - 'size.array' => 'The :attribute must contain :size items.', - 'unique' => 'The :attribute has already been taken.', - 'string' => 'The :attribute must be a string.', - 'url' => 'The :attribute format is invalid.', - 'timezone' => 'The :attribute must be a valid zone.', - '2fa_code' => 'The :attribute field is invalid.', - 'dimensions' => 'The :attribute has invalid image dimensions.', - 'distinct' => 'The :attribute field has a duplicate value.', - 'file' => 'The :attribute must be a file.', - 'in_array' => 'The :attribute field does not exist in :other.', - 'present' => 'The :attribute field must be present.', - 'amount_zero' => 'The total amount cannot be zero.', - 'current_target_amount' => 'The current amount must be less than the target amount.', - 'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.', - 'unique_object_group' => 'The group name must be unique', - 'starts_with' => 'The value must start with :values.', - 'unique_webhook' => 'You already have a webhook with this combination of URL, trigger, response and delivery.', - 'unique_existing_webhook' => 'You already have another webhook with this combination of URL, trigger, response and delivery.', - 'same_account_type' => 'Both accounts must be of the same account type', - 'same_account_currency' => 'Both accounts must have the same currency setting', - 'piggy_no_change_currency' => 'Because there are piggy banks linked to this account, you cannot change the currency of the account.', + 'between.numeric' => 'The :attribute must be between :min and :max.', + 'between.file' => 'The :attribute must be between :min and :max kilobytes.', + 'between.string' => 'The :attribute must be between :min and :max characters.', + 'between.array' => 'The :attribute must have between :min and :max items.', + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'email' => 'The :attribute must be a valid email address.', + 'filled' => 'The :attribute field is required.', + 'exists' => 'The selected :attribute is invalid.', + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'max.numeric' => 'The :attribute may not be greater than :max.', + 'max.file' => 'The :attribute may not be greater than :max kilobytes.', + 'max.string' => 'The :attribute may not be greater than :max characters.', + 'max.array' => 'The :attribute may not have more than :max items.', + 'mimes' => 'The :attribute must be a file of type: :values.', + 'min.numeric' => 'The :attribute must be at least :min.', + 'lte.numeric' => 'The :attribute must be less than or equal :value.', + 'min.file' => 'The :attribute must be at least :min kilobytes.', + 'min.string' => 'The :attribute must be at least :min characters.', + 'min.array' => 'The :attribute must have at least :min items.', + 'not_in' => 'The selected :attribute is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'convert_to_itself' => 'Cannot store currency exchange rate for ":code", because from and to currency are the same.', + 'invalid_currency_code' => 'Currency code ":code" is invalid', + 'scientific_notation' => 'The :attribute cannot use the scientific notation.', + 'numeric_primary' => 'The primary currency amount must be a number.', + 'numeric_destination' => 'The destination amount must be a number.', + 'numeric_source' => 'The source amount must be a number.', + 'generic_invalid' => 'This value is invalid.', + 'transaction_type_changed' => 'If you change the type of the transaction, make sure the correct source/destination accounts are set.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values is present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size.numeric' => 'The :attribute must be :size.', + 'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.', + 'size.file' => 'The :attribute must be :size kilobytes.', + 'size.string' => 'The :attribute must be :size characters.', + 'size.array' => 'The :attribute must contain :size items.', + 'unique' => 'The :attribute has already been taken.', + 'string' => 'The :attribute must be a string.', + 'url' => 'The :attribute format is invalid.', + 'timezone' => 'The :attribute must be a valid zone.', + '2fa_code' => 'The :attribute field is invalid.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'file' => 'The :attribute must be a file.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'present' => 'The :attribute field must be present.', + 'amount_zero' => 'The total amount cannot be zero.', + 'current_target_amount' => 'The current amount must be less than the target amount.', + 'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.', + 'unique_object_group' => 'The group name must be unique', + 'starts_with' => 'The value must start with :values.', + 'unique_webhook' => 'You already have a webhook with this combination of URL, trigger, response and delivery.', + 'unique_existing_webhook' => 'You already have another webhook with this combination of URL, trigger, response and delivery.', + 'same_account_type' => 'Both accounts must be of the same account type', + 'same_account_currency' => 'Both accounts must have the same currency setting', + 'piggy_no_change_currency' => 'Because there are piggy banks linked to this account, you cannot change the currency of the account.', - - 'secure_password' => 'This is not a secure password. Please try again. For more information, visit https://bit.ly/FF3-password', - 'valid_recurrence_rep_type' => 'Invalid repetition type for recurring transactions.', - 'valid_recurrence_rep_moment' => 'Invalid repetition moment for this type of repetition.', - 'invalid_account_info' => 'Invalid account information.', - 'attributes' => [ + 'secure_password' => 'This is not a secure password. Please try again. For more information, visit https://bit.ly/FF3-password', + 'valid_recurrence_rep_type' => 'Invalid repetition type for recurring transactions.', + 'valid_recurrence_rep_moment' => 'Invalid repetition moment for this type of repetition.', + 'invalid_account_info' => 'Invalid account information.', + 'attributes' => [ 'email' => 'email address', 'description' => 'description', 'amount' => 'amount', @@ -227,59 +226,58 @@ return [ ], // validation of accounts: - 'withdrawal_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', - 'withdrawal_source_bad_data' => '[a] Could not find a valid source account when searching for ID ":id" or name ":name".', - 'withdrawal_dest_need_data' => '[a] Need to get a valid destination account ID and/or valid destination account name to continue.', - 'withdrawal_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', + 'withdrawal_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', + 'withdrawal_source_bad_data' => '[a] Could not find a valid source account when searching for ID ":id" or name ":name".', + 'withdrawal_dest_need_data' => '[a] Need to get a valid destination account ID and/or valid destination account name to continue.', + 'withdrawal_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', - 'withdrawal_dest_iban_exists' => 'This destination account IBAN is already in use by an asset account or a liability and cannot be used as a withdrawal destination.', - 'deposit_src_iban_exists' => 'This source account IBAN is already in use by an asset account or a liability and cannot be used as a deposit source.', + 'withdrawal_dest_iban_exists' => 'This destination account IBAN is already in use by an asset account or a liability and cannot be used as a withdrawal destination.', + 'deposit_src_iban_exists' => 'This source account IBAN is already in use by an asset account or a liability and cannot be used as a deposit source.', - 'reconciliation_source_bad_data' => 'Could not find a valid reconciliation account when searching for ID ":id" or name ":name".', + 'reconciliation_source_bad_data' => 'Could not find a valid reconciliation account when searching for ID ":id" or name ":name".', - 'generic_source_bad_data' => '[e] Could not find a valid source account when searching for ID ":id" or name ":name".', + 'generic_source_bad_data' => '[e] Could not find a valid source account when searching for ID ":id" or name ":name".', - 'deposit_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', - 'deposit_source_bad_data' => '[b] Could not find a valid source account when searching for ID ":id" or name ":name".', - 'deposit_dest_need_data' => '[b] Need to get a valid destination account ID and/or valid destination account name to continue.', - 'deposit_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', - 'deposit_dest_wrong_type' => 'The submitted destination account is not of the right type.', + 'deposit_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', + 'deposit_source_bad_data' => '[b] Could not find a valid source account when searching for ID ":id" or name ":name".', + 'deposit_dest_need_data' => '[b] Need to get a valid destination account ID and/or valid destination account name to continue.', + 'deposit_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', + 'deposit_dest_wrong_type' => 'The submitted destination account is not of the right type.', + 'transfer_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', + 'transfer_source_bad_data' => '[c] Could not find a valid source account when searching for ID ":id" or name ":name".', + 'transfer_dest_need_data' => '[c] Need to get a valid destination account ID and/or valid destination account name to continue.', + 'transfer_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', + 'need_id_in_edit' => 'Each split must have transaction_journal_id (either valid ID or 0).', - 'transfer_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', - 'transfer_source_bad_data' => '[c] Could not find a valid source account when searching for ID ":id" or name ":name".', - 'transfer_dest_need_data' => '[c] Need to get a valid destination account ID and/or valid destination account name to continue.', - 'transfer_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', - 'need_id_in_edit' => 'Each split must have transaction_journal_id (either valid ID or 0).', + 'ob_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', + 'lc_source_need_data' => 'Need to get a valid source account ID to continue.', + 'ob_dest_need_data' => '[d] Need to get a valid destination account ID and/or valid destination account name to continue.', + 'ob_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', + 'reconciliation_either_account' => 'To submit a reconciliation, you must submit either a source or a destination account. Not both, not neither.', - 'ob_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', - 'lc_source_need_data' => 'Need to get a valid source account ID to continue.', - 'ob_dest_need_data' => '[d] Need to get a valid destination account ID and/or valid destination account name to continue.', - 'ob_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', - 'reconciliation_either_account' => 'To submit a reconciliation, you must submit either a source or a destination account. Not both, not neither.', + 'generic_invalid_source' => 'You can\'t use this account as the source account.', + 'generic_invalid_destination' => 'You can\'t use this account as the destination account.', - 'generic_invalid_source' => 'You can\'t use this account as the source account.', - 'generic_invalid_destination' => 'You can\'t use this account as the destination account.', + 'generic_no_source' => 'You must submit source account information or submit a transaction journal ID.', + 'generic_no_destination' => 'You must submit destination account information or submit a transaction journal ID.', - 'generic_no_source' => 'You must submit source account information or submit a transaction journal ID.', - 'generic_no_destination' => 'You must submit destination account information or submit a transaction journal ID.', + 'gte.numeric' => 'The :attribute must be greater than or equal to :value.', + 'gt.numeric' => 'The :attribute must be greater than :value.', + 'gte.file' => 'The :attribute must be greater than or equal to :value kilobytes.', + 'gte.string' => 'The :attribute must be greater than or equal to :value characters.', + 'gte.array' => 'The :attribute must have :value items or more.', + 'missing_with' => 'The :attribute cannot be combined with another field.', - 'gte.numeric' => 'The :attribute must be greater than or equal to :value.', - 'gt.numeric' => 'The :attribute must be greater than :value.', - 'gte.file' => 'The :attribute must be greater than or equal to :value kilobytes.', - 'gte.string' => 'The :attribute must be greater than or equal to :value characters.', - 'gte.array' => 'The :attribute must have :value items or more.', - 'missing_with' => 'The :attribute cannot be combined with another field.', + 'amount_required_for_auto_budget' => 'The amount is required.', + 'auto_budget_amount_positive' => 'The amount must be more than zero.', - 'amount_required_for_auto_budget' => 'The amount is required.', - 'auto_budget_amount_positive' => 'The amount must be more than zero.', - - 'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.', + 'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.', // no access to administration: - 'no_auth_user_group' => 'You have to be logged in to access this administration.', - 'no_access_user_group' => 'You do not have the correct access rights for this administration.', - 'administration_owner_rename' => 'You can\'t rename your standard administration.', - 'existing_mfa_code' => 'Please enter a valid code', + 'no_auth_user_group' => 'You have to be logged in to access this administration.', + 'no_access_user_group' => 'You do not have the correct access rights for this administration.', + 'administration_owner_rename' => 'You can\'t rename your standard administration.', + 'existing_mfa_code' => 'Please enter a valid code', ]; diff --git a/routes/api.php b/routes/api.php index fc8ed57ab2..09a72d07f4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -33,6 +33,10 @@ use Illuminate\Support\Facades\Route; * \__/ |_| | _| `._____| \______/ \______/ |__| |_______|_______/ */ +if (!defined('DATEFORMAT')) { + define('DATEFORMAT', '(19|20)[0-9]{2}-?[0-9]{2}-?[0-9]{2}'); +} + // Autocomplete controllers Route::group( [ @@ -69,24 +73,34 @@ Route::group( 'as' => 'api.v1.exchange-rates.', ], static function (): void { + // get all Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']); - Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']); - Route::get('{userGroupExchangeRate}', ['uses' => 'ShowController@showSingle', 'as' => 'show.single']); - Route::delete('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); - Route::delete('{userGroupExchangeRate}', ['uses' => 'DestroyController@destroySingle', 'as' => 'destroy.single']); - Route::put('{userGroupExchangeRate}', ['uses' => 'UpdateController@update', 'as' => 'update']); + // get list of rates + Route::get('{userGroupExchangeRate}', ['uses' => 'ShowController@showSingleById', 'as' => 'show.single']); + Route::get('{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']); + Route::get('{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'ShowController@showSingleByDate', 'as' => 'show.by-date'])->where(['start_date' => DATEFORMAT]); + + // delete all rates + Route::delete('{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); + // delete single rate + Route::delete('{userGroupExchangeRate}', ['uses' => 'DestroyController@destroySingleById', 'as' => 'destroy.single']); + Route::delete('{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'DestroyController@destroySingleByDate', 'as' => 'destroy.by-date'])->where(['start_date' => DATEFORMAT]); + + // update single + Route::put('{userGroupExchangeRate}', ['uses' => 'UpdateController@updateById', 'as' => 'update']); + Route::put('{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'UpdateController@updateByDate', 'as' => 'update.by-date'])->where(['start_date' => DATEFORMAT]); + + // post new rate Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']); + Route::post('by-date/{date}', ['uses' => 'StoreController@storeByDate', 'as' => 'store.by-date'])->where(['start_date' => DATEFORMAT]); + Route::post('by-currencies/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'StoreController@storeByCurrencies', 'as' => 'store.by-currencies']); } ); -// CHART ROUTES. - -// chart balance - // CHART ROUTES Route::group( [ - 'namespace' => 'FireflyIII\Api\V2\Controllers\Chart', + 'namespace' => 'FireflyIII\Api\V1\Controllers\Chart', 'prefix' => 'v1/chart/balance', 'as' => 'api.v1.chart.balance', ], @@ -104,7 +118,6 @@ Route::group( ], static function (): void { Route::get('overview', ['uses' => 'AccountController@overview', 'as' => 'overview']); - Route::get('dashboard', ['uses' => 'AccountController@dashboard', 'as' => 'dashboard']); } ); @@ -115,7 +128,7 @@ Route::group( 'as' => 'api.v1.chart.budget.', ], static function (): void { - Route::get('dashboard', ['uses' => 'BudgetController@dashboard', 'as' => 'dashboard']); + Route::get('overview', ['uses' => 'BudgetController@overview', 'as' => 'overview']); } ); @@ -126,7 +139,7 @@ Route::group( 'as' => 'api.v1.chart.category.', ], static function (): void { - Route::get('dashboard', ['uses' => 'CategoryController@dashboard', 'as' => 'dashboard']); + Route::get('overview', ['uses' => 'CategoryController@overview', 'as' => 'overview']); } ); diff --git a/temp-file.txt b/temp-file.txt deleted file mode 100644 index edd8488031..0000000000 --- a/temp-file.txt +++ /dev/null @@ -1 +0,0 @@ -Sun Mar 16 16:48:09 UTC 2025