From d3032054654fa223a3191f9acec22ff0fcd11931 Mon Sep 17 00:00:00 2001 From: Sander Dorigo Date: Mon, 24 Aug 2026 14:28:15 +0200 Subject: [PATCH] Expand transaction links --- .../Autocomplete/TransactionController.php | 39 +++++++++++++++++++ .../TransactionGroup/StoresAuditLogEntry.php | 9 ++++- app/Support/Amount.php | 9 +++++ .../assets/v3/js/pages/transactions/create.js | 33 +++++++++++++--- .../components/transaction/split.blade.php | 2 +- routes/api.php | 1 + 6 files changed, 86 insertions(+), 7 deletions(-) diff --git a/app/Api/V1/Controllers/Autocomplete/TransactionController.php b/app/Api/V1/Controllers/Autocomplete/TransactionController.php index df3e36b45c..b5982050e0 100644 --- a/app/Api/V1/Controllers/Autocomplete/TransactionController.php +++ b/app/Api/V1/Controllers/Autocomplete/TransactionController.php @@ -32,6 +32,8 @@ use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Facades\Steam; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Collection; @@ -119,4 +121,41 @@ final class TransactionController extends Controller return response()->api($array); } + + public function transactionsWithMeta(AutocompleteApiRequest $request): JsonResponse + { + $result = new Collection(); + if (is_numeric($request->attributes->get('query'))) { + // search for group, not journal. + $firstResult = $this->groupRepository->find((int) $request->attributes->get('query')); + if ($firstResult instanceof TransactionGroup) { + // group may contain multiple journals, each a result: + foreach ($firstResult->transactionJournals as $journal) { + $result->push($journal); + } + } + } + if (!is_numeric($request->attributes->get('query'))) { + $result = $this->repository->searchJournalDescriptions($request->attributes->get('query'), $request->attributes->get('limit')); + } + + // limit and unique + $array = []; + + /** @var TransactionJournal $journal */ + foreach ($result as $journal) { + $currency = Amount::getCurrencyFromJournal($journal); + $array[] = [ + 'id' => (string) $journal->id, + 'transaction_group_id' => (string) $journal->transaction_group_id, + 'name' => $journal->description, + 'description' => $journal->description, + 'date' => $journal->date, + 'currency_code' => $currency->code, + 'amount' => Steam::positive(Amount::getAmountFromJournalObject($journal)), + ]; + } + + return response()->api($array); + } } diff --git a/app/Listeners/Model/TransactionGroup/StoresAuditLogEntry.php b/app/Listeners/Model/TransactionGroup/StoresAuditLogEntry.php index 2e86fac55d..5edde2f69a 100644 --- a/app/Listeners/Model/TransactionGroup/StoresAuditLogEntry.php +++ b/app/Listeners/Model/TransactionGroup/StoresAuditLogEntry.php @@ -41,11 +41,18 @@ class StoresAuditLogEntry implements ShouldQueue 'before' => $event->before, 'after' => $event->after, ]; - if ('' === trim((string) $event->before) && '' === trim((string) $event->after)) { + if (!is_array($event->before) && !is_array($event->after) && '' === trim((string) $event->before) && '' === trim((string) $event->after)) { Log::debug('Will not store event log because before and after are both empty.'); return; } + // both empty arrays: + if (is_array($event->before) && is_array($event->after) && 0 === count($event->before) && 0 === count($event->after)) { + Log::debug('Will not store event log because before and after are both empty array.'); + + return; + } + if ($event->before === $event->after) { Log::debug('Will not store event log because before and after are the same.'); diff --git a/app/Support/Amount.php b/app/Support/Amount.php index c9463093ec..bedc46bac6 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -219,6 +219,15 @@ class Amount return (string) $amount; } + public function getCurrencyFromJournal(TransactionJournal $journal): TransactionCurrency { + /** @var Transaction|null $first */ + $first = $journal->transactions->first(); + if(null === $first){ + return $this->getPrimaryCurrency(); + } + return $first->transactionCurrency; + } + /** * Experimental function to see if we can quickly and quietly get the amount from a journal. * This depends on the user's default currency and the wish to have it converted. diff --git a/resources/assets/v3/js/pages/transactions/create.js b/resources/assets/v3/js/pages/transactions/create.js index 9df64d3dfc..944f20e352 100644 --- a/resources/assets/v3/js/pages/transactions/create.js +++ b/resources/assets/v3/js/pages/transactions/create.js @@ -54,6 +54,7 @@ import {clearLocation} from './shared/clear-location.js'; import bootstrap from "bootstrap/dist/js/bootstrap.bundle.js"; import {removeSplit} from "./shared/remove-split.js"; import {loadTransactionLinks} from './shared/load-transaction-links.js'; +import Autocomplete from "bootstrap5-autocomplete"; let create = function () { @@ -244,11 +245,31 @@ let create = function () { // TODO don't do this on a timeout! }, 500); }, - manageTransactionLinks() { - console.log('here we are!'); - let modal1 = bootstrap.Modal.getOrCreateInstance('#linksModal'); - modal1.show(); - }, + createAutocomplete(fieldIdentifier, url) { + let token = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + + const renderJournal = function (item, b, c) { + return item.description + '
'+item.currency_code +' '+item.amount+ ' @ ' +item.date+''; + }; + console.log('Make autocomplete.'); + Autocomplete.init('#' + fieldIdentifier, { + + server: url + '?_token=' + token, + labelField: 'name', + valueField: 'id', + liveServer: true, + onRenderItem: renderJournal, + fetchOptions: { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRF-TOKEN': token + } + } + }); + }, init() { this.i18next = i18next; @@ -286,6 +307,8 @@ let create = function () { //console.log(data); this.formData.linkTypes = data; this.formStates.loadingLinks = false; + this.createAutocomplete('linksModal_search_0','api/v1/autocomplete/transactions-with-meta'); + }); } }); diff --git a/resources/views/components/transaction/split.blade.php b/resources/views/components/transaction/split.blade.php index ed63337901..51c174812e 100644 --- a/resources/views/components/transaction/split.blade.php +++ b/resources/views/components/transaction/split.blade.php @@ -193,7 +193,7 @@
- +
diff --git a/routes/api.php b/routes/api.php index 62365034ae..2181ee3917 100644 --- a/routes/api.php +++ b/routes/api.php @@ -85,6 +85,7 @@ Route::group( Route::get('tags', ['uses' => 'TagController@tags', 'as' => 'tags']); Route::get('transactions', ['uses' => 'TransactionController@transactions', 'as' => 'transactions']); Route::get('transactions-with-id', ['uses' => 'TransactionController@transactionsWithID', 'as' => 'transactions-with-id']); + Route::get('transactions-with-meta', ['uses' => 'TransactionController@transactionsWithMeta', 'as' => 'transactions-with-meta']); Route::get('transaction-types', ['uses' => 'TransactionTypeController@transactionTypes', 'as' => 'transaction-types']); } );