diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index bb602229d3..e9b63f4b17 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -14,7 +14,7 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers; use FireflyIII\Support\Search\SearchInterface; -use Input; +use Illuminate\Http\Request; /** * Class SearchController @@ -30,12 +30,6 @@ class SearchController extends Controller { parent::__construct(); - $this->middleware( - function ($request, $next) { - - return $next($request); - } - ); } /** @@ -45,16 +39,21 @@ class SearchController extends Controller * * @return $this */ - public function index(SearchInterface $searcher) + public function index(Request $request, SearchInterface $searcher) { - + $minSearchLen = 1; $subTitle = null; $query = null; $result = []; $title = trans('firefly.search'); + $limit = 20; $mainTitleIcon = 'fa-search'; - if (!is_null(Input::get('q')) && strlen(Input::get('q')) > 0) { - $query = trim(Input::get('q')); + + // set limit for search: + $searcher->setLimit($limit); + + if (!is_null($request->get('q')) && strlen($request->get('q')) >= $minSearchLen) { + $query = trim(strtolower($request->get('q'))); $words = explode(' ', $query); $subTitle = trans('firefly.search_results_for', ['query' => $query]); @@ -67,7 +66,7 @@ class SearchController extends Controller } - return view('search.index', compact('title', 'subTitle', 'mainTitleIcon', 'query', 'result')); + return view('search.index', compact('title', 'subTitle', 'limit', 'mainTitleIcon', 'query', 'result')); } } diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index b1530b08ca..91122f0d5f 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -94,7 +94,6 @@ class FireflyServiceProvider extends ServiceProvider ); $this->app->bind('FireflyIII\Repositories\Currency\CurrencyRepositoryInterface', 'FireflyIII\Repositories\Currency\CurrencyRepository'); - $this->app->bind('FireflyIII\Support\Search\SearchInterface', 'FireflyIII\Support\Search\Search'); $this->app->bind('FireflyIII\Repositories\User\UserRepositoryInterface', 'FireflyIII\Repositories\User\UserRepository'); $this->app->bind('FireflyIII\Helpers\Attachments\AttachmentHelperInterface', 'FireflyIII\Helpers\Attachments\AttachmentHelper'); $this->app->bind( diff --git a/app/Providers/SearchServiceProvider.php b/app/Providers/SearchServiceProvider.php new file mode 100644 index 0000000000..a7be767e5e --- /dev/null +++ b/app/Providers/SearchServiceProvider.php @@ -0,0 +1,59 @@ +app->bind( + 'FireflyIII\Support\Search\SearchInterface', + function (Application $app, array $arguments) { + if (!isset($arguments[0]) && $app->auth->check()) { + return app('FireflyIII\Support\Search\Search', [auth()->user()]); + } + if (!isset($arguments[0]) && !$app->auth->check()) { + throw new FireflyException('There is no user present.'); + } + + return app('FireflyIII\Support\Search\Search', $arguments); + } + ); + } +} diff --git a/app/Rules/TransactionMatcher.php b/app/Rules/TransactionMatcher.php index eb845dee29..0e63c06085 100644 --- a/app/Rules/TransactionMatcher.php +++ b/app/Rules/TransactionMatcher.php @@ -78,7 +78,7 @@ class TransactionMatcher do { // Fetch a batch of transactions from the database $collector = new JournalCollector(auth()->user()); - $collector->setAllAssetAccounts()->setLimit($pageSize * 2)->setPage($page)->setTypes($this->transactionTypes); + $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page)->setTypes($this->transactionTypes); $set = $collector->getPaginatedJournals(); Log::debug(sprintf('Found %d journals to check. ', $set->count())); @@ -105,7 +105,7 @@ class TransactionMatcher Log::debug(sprintf('Page is now %d, processed is %d', $page, $processed)); // Check for conditions to finish the loop - $reachedEndOfList = $set->count() < $pageSize; + $reachedEndOfList = $set->count() < 1; $foundEnough = $result->count() >= $this->limit; $searchedEnough = ($processed >= $this->range); diff --git a/app/Support/Search/Search.php b/app/Support/Search/Search.php index d20e83413f..c5e1da96c7 100644 --- a/app/Support/Search/Search.php +++ b/app/Support/Search/Search.php @@ -14,11 +14,15 @@ declare(strict_types = 1); namespace FireflyIII\Support\Search; +use FireflyIII\Helpers\Collector\JournalCollector; +use FireflyIII\Models\Account; use FireflyIII\Models\Budget; use FireflyIII\Models\Category; -use FireflyIII\Models\TransactionJournal; -use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use FireflyIII\Models\Tag; +use FireflyIII\Models\Transaction; +use FireflyIII\User; use Illuminate\Support\Collection; +use Log; /** * Class Search @@ -27,20 +31,46 @@ use Illuminate\Support\Collection; */ class Search implements SearchInterface { + /** @var int */ + private $limit = 100; + /** @var User */ + private $user; + /** + * AttachmentRepository constructor. + * + * @param User $user + */ + public function __construct(User $user) + { + $this->user = $user; + } + + /** + * The search will assume that the user does not have so many accounts + * that this search should be paginated. + * * @param array $words * * @return Collection */ public function searchAccounts(array $words): Collection { - return auth()->user()->accounts()->with('accounttype')->where( - function (EloquentBuilder $q) use ($words) { - foreach ($words as $word) { - $q->orWhere('name', 'LIKE', '%' . e($word) . '%'); + $accounts = $this->user->accounts()->get(); + /** @var Collection $result */ + $result = $accounts->filter( + function (Account $account) use ($words) { + if ($this->strpos_arr(strtolower($account->name), $words)) { + return $account; } + + return false; } - )->get(); + ); + + $result = $result->slice(0, $this->limit); + + return $result; } /** @@ -51,46 +81,46 @@ class Search implements SearchInterface public function searchBudgets(array $words): Collection { /** @var Collection $set */ - $set = auth()->user()->budgets()->get(); - $newSet = $set->filter( - function (Budget $b) use ($words) { - $found = 0; - foreach ($words as $word) { - if (!(strpos(strtolower($b->name), strtolower($word)) === false)) { - $found++; - } + $set = auth()->user()->budgets()->get(); + /** @var Collection $result */ + $result = $set->filter( + function (Budget $budget) use ($words) { + if ($this->strpos_arr(strtolower($budget->name), $words)) { + return $budget; } - return $found > 0; + return false; } ); - return $newSet; + $result = $result->slice(0, $this->limit); + + return $result; } /** + * Search assumes the user does not have that many categories. So no paginated search. + * * @param array $words * * @return Collection */ public function searchCategories(array $words): Collection { - /** @var Collection $set */ - $set = auth()->user()->categories()->get(); - $newSet = $set->filter( - function (Category $c) use ($words) { - $found = 0; - foreach ($words as $word) { - if (!(strpos(strtolower($c->name), strtolower($word)) === false)) { - $found++; - } + $categories = $this->user->categories()->get(); + /** @var Collection $result */ + $result = $categories->filter( + function (Category $category) use ($words) { + if ($this->strpos_arr(strtolower($category->name), $words)) { + return $category; } - return $found > 0; + return false; } ); + $result = $result->slice(0, $this->limit); - return $newSet; + return $result; } /** @@ -101,7 +131,21 @@ class Search implements SearchInterface */ public function searchTags(array $words): Collection { - return new Collection; + $tags = $this->user->tags()->get(); + + /** @var Collection $result */ + $result = $tags->filter( + function (Tag $tag) use ($words) { + if ($this->strpos_arr(strtolower($tag->tag), $words)) { + return $tag; + } + + return false; + } + ); + $result = $result->slice(0, $this->limit); + + return $result; } /** @@ -111,40 +155,86 @@ class Search implements SearchInterface */ public function searchTransactions(array $words): Collection { - // decrypted transaction journals: - $decrypted = auth()->user()->transactionJournals()->expanded()->where('transaction_journals.encrypted', 0)->where( - function (EloquentBuilder $q) use ($words) { - foreach ($words as $word) { - $q->orWhere('transaction_journals.description', 'LIKE', '%' . e($word) . '%'); - } - } - )->get(TransactionJournal::queryFields()); + $pageSize = 100; + $processed = 0; + $page = 1; + $result = new Collection(); + do { + $collector = new JournalCollector($this->user); + $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page); + $set = $collector->getPaginatedJournals(); + Log::debug(sprintf('Found %d journals to check. ', $set->count())); - // encrypted - $all = auth()->user()->transactionJournals()->expanded()->where('transaction_journals.encrypted', 1)->get(TransactionJournal::queryFields()); - $set = $all->filter( - function (TransactionJournal $journal) use ($words) { - foreach ($words as $word) { - $haystack = strtolower($journal->description); - $word = strtolower($word); - if (!(strpos($haystack, $word) === false)) { - return $journal; + // Filter transactions that match the given triggers. + $filtered = $set->filter( + function (Transaction $transaction) use ($words) { + // check descr of journal: + if ($this->strpos_arr(strtolower(strval($transaction->description)), $words)) { + return $transaction; } + + // check descr of transaction + if ($this->strpos_arr(strtolower(strval($transaction->transaction_description)), $words)) { + return $transaction; + } + + // return false: + return false; } + ); - return null; + Log::debug(sprintf('Found %d journals that match.', $filtered->count())); + // merge: + /** @var Collection $result */ + $result = $result->merge($filtered); + Log::debug(sprintf('Total count is now %d', $result->count())); + + // Update counters + $page++; + $processed += count($set); + + Log::debug(sprintf('Page is now %d, processed is %d', $page, $processed)); + + // Check for conditions to finish the loop + $reachedEndOfList = $set->count() < 1; + $foundEnough = $result->count() >= $this->limit; + + Log::debug(sprintf('reachedEndOfList: %s', var_export($reachedEndOfList, true))); + Log::debug(sprintf('foundEnough: %s', var_export($foundEnough, true))); + + } while (!$reachedEndOfList && !$foundEnough); + + $result = $result->slice(0, $this->limit); + + return $result; + } + + /** + * @param int $limit + */ + public function setLimit(int $limit) + { + $this->limit = $limit; + } + + /** + * @param string $haystack + * @param array $needle + * + * @return bool + */ + private function strpos_arr(string $haystack, array $needle) + { + if (strlen($haystack) === 0) { + return false; + } + foreach ($needle as $what) { + if (($pos = strpos($haystack, $what)) !== false) { + return true; } - ); - $filtered = $set->merge($decrypted); - $filtered = $filtered->sortBy( - function (TransactionJournal $journal) { - return intval($journal->date->format('U')); - } - ); + } - $filtered = $filtered->reverse(); - - return $filtered; + return false; } } diff --git a/config/app.php b/config/app.php index 0611c68b7f..478fae8b8d 100755 --- a/config/app.php +++ b/config/app.php @@ -208,6 +208,7 @@ return [ FireflyIII\Providers\PiggyBankServiceProvider::class, FireflyIII\Providers\RuleServiceProvider::class, FireflyIII\Providers\RuleGroupServiceProvider::class, + FireflyIII\Providers\SearchServiceProvider::class, FireflyIII\Providers\TagServiceProvider::class, diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 8519610ca7..b7643c4751 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -67,7 +67,19 @@ return [ 'warning_much_data' => ':days days of data may take a while to load.', 'registered' => 'You have registered successfully!', 'search' => 'Search', + 'search_found_accounts' => 'Found :count account(s) for your query.', + 'search_found_categories' => 'Found :count category(ies) for your query.', + 'search_found_budgets' => 'Found :count budget(s) for your query.', + 'search_found_tags' => 'Found :count tag(s) for your query.', + 'search_found_transactions' => 'Found :count transaction(s) for your query.', + 'results_limited' => 'The results are limited to :count entries.', + 'tagbalancingAct' => 'Balancing act', + 'tagadvancePayment' => 'Advance payment', + 'tagnothing' => '', + 'Default asset account' => 'Default asset account', 'no_budget_pointer' => 'You seem to have no budgets yet. You should create some on the budgets-page. Budgets can help you keep track of expenses.', + 'Savings account' => 'Savings account', + 'Credit card' => 'Credit card', 'source_accounts' => 'Source account(s)', 'destination_accounts' => 'Destination account(s)', 'user_id_is' => 'Your user id is :user', diff --git a/resources/views/search/index.twig b/resources/views/search/index.twig index 35bf5a3d28..88107c52d6 100644 --- a/resources/views/search/index.twig +++ b/resources/views/search/index.twig @@ -5,19 +5,35 @@ {% endblock %} {% block content %} - {% if query %} + {% if query == "" %}
+
+

{{ 'no_results_for_empty_search'|_ }}

+
+
+ {% endif %} + {% if query %} + +
+
+

{{ trans('firefly.results_limited', {count: limit}) }}

+
+
+ +
+ + {% if result.transactions|length > 0 %}
-

Transactions

+

{{ 'transactions'|_ }}

-
- {% include 'list.journals-tiny' with {'transactions' : result.transactions} %} -
-
@@ -26,19 +42,13 @@
-

Categories

+

{{ 'categories'|_ }}

-
-
- {% for category in result.categories %} - - {{ category.name }} - - {% endfor %} -
-
-
@@ -47,13 +57,13 @@
-

Tags

+

{{ 'tags'|_ }}

-
-

Bla bla

-
-
@@ -62,19 +72,13 @@
-

Accounts

+

{{ 'accounts'|_ }}

-
-
- {% for account in result.accounts %} - - {{ account.name }} - - {% endfor %} -
-
-
@@ -83,19 +87,13 @@
-

Budgets

+

{{ 'budgets'|_ }}

-
- {% for budget in result.budgets %} - - {{ budget.name }} - - {% endfor %} -
-
-
@@ -106,8 +104,8 @@ {% endblock %} -{% block scripts %} - -{% endblock %} + {% block scripts %} + + {% endblock %} diff --git a/resources/views/search/partials/accounts.twig b/resources/views/search/partials/accounts.twig new file mode 100644 index 0000000000..e21b443d59 --- /dev/null +++ b/resources/views/search/partials/accounts.twig @@ -0,0 +1,34 @@ + + + + + + + + + + + + {% for account in result.accounts %} + + + + + + + + {% endfor %} + + +
{{ trans('list.name') }}
{{ account.name }}{{ trans('firefly.'~account.accountType.type) }}
diff --git a/resources/views/search/partials/budgets.twig b/resources/views/search/partials/budgets.twig new file mode 100644 index 0000000000..ad14e57747 --- /dev/null +++ b/resources/views/search/partials/budgets.twig @@ -0,0 +1,22 @@ + + + + + + + + + {% for budget in result.budgets %} + + + + + {% endfor %} + + +
{{ trans('list.name') }}
{{ budget.name }}
diff --git a/resources/views/search/partials/categories.twig b/resources/views/search/partials/categories.twig new file mode 100644 index 0000000000..4176de3240 --- /dev/null +++ b/resources/views/search/partials/categories.twig @@ -0,0 +1,22 @@ + + + + + + + + + {% for category in result.categories %} + + + + + {% endfor %} + + +
{{ trans('list.name') }}
{{ category.name }}
diff --git a/resources/views/search/partials/tags.twig b/resources/views/search/partials/tags.twig new file mode 100644 index 0000000000..c4b3e21a2c --- /dev/null +++ b/resources/views/search/partials/tags.twig @@ -0,0 +1,24 @@ + + + + + + + + + + {% for tag in result.tags %} + + + + + + {% endfor %} + + +
{{ trans('list.name') }}{{ trans('list.type') }}
{{ tag.tag }}{{ ('tag'~tag.tagMode)|_ }}
diff --git a/resources/views/search/partials/transactions.twig b/resources/views/search/partials/transactions.twig new file mode 100644 index 0000000000..c20da1ff07 --- /dev/null +++ b/resources/views/search/partials/transactions.twig @@ -0,0 +1,81 @@ +{{ journals.render|raw }} + + + + + + + + + + + + {% for transaction in transactions %} + + + + + + + + {% endfor %} + +
{{ trans('list.description') }}{{ trans('list.amount') }}
+ + + {% if transaction.transaction_description|length > 0 %} + {{ transaction.transaction_description }} ({{ transaction.description }}) + {% else %} + {{ transaction.description }} + {% endif %} + + {{ splitJournalIndicator(transaction.journal_id) }} + + {% if transaction.transactionJournal.attachments|length > 0 %} + + {% endif %} + + + + {{ formatAmountWithCode(transaction.transaction_amount, transaction.transaction_currency_code) }} + + {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, + transaction.transaction_currency_code, transaction.transaction_type_type) }} + + +
+ +
+
+ {{ journals.render|raw }} +
+
+