diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index 373c4fa72f..453ff2d78a 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -18,13 +18,17 @@ use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Requests\CategoryFormRequest; use FireflyIII\Models\AccountType; use FireflyIII\Models\Category; +use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Log; use Navigation; use Preferences; +use Steam; use View; /** @@ -152,24 +156,78 @@ class CategoryController extends Controller /** * @return View */ - public function noCategory() + public function noCategory(Request $request, JournalRepositoryInterface $repository, string $moment = '') { - /** @var Carbon $start */ - $start = session('start', Carbon::now()->startOfMonth()); - /** @var Carbon $end */ - $end = session('end', Carbon::now()->startOfMonth()); + // default values: + $range = Preferences::get('viewRange', '1M')->data; + $start = null; + $end = null; + $periods = new Collection; - // new collector: - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setAllAssetAccounts()->setRange($start, $end)->withoutCategory();//->groupJournals(); - $journals = $collector->getJournals(); - $subTitle = trans( - 'firefly.without_category_between', - ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] - ); + // prep for "all" view. + if ($moment === 'all') { + $subTitle = trans('firefly.all_journals_without_category'); + $first = $repository->first(); + $start = $first->date ?? new Carbon; + $end = new Carbon; + } - return view('categories.no-category', compact('journals', 'subTitle')); + // prep for "specific date" view. + if (strlen($moment) > 0 && $moment !== 'all') { + $start = new Carbon($moment); + $end = Navigation::endOfPeriod($start, $range); + $subTitle = trans( + 'firefly.without_category_between', + ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] + ); + $periods = $this->noCategoryPeriodEntries(); + } + + // prep for current period + if (strlen($moment) === 0) { + $start = clone session('start', Navigation::startOfPeriod(new Carbon, $range)); + $end = clone session('end', Navigation::endOfPeriod(new Carbon, $range)); + $periods = $this->noCategoryPeriodEntries(); + $subTitle = trans( + 'firefly.without_category_between', + ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] + ); + } + + $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + + $count = 0; + $loop = 0; + // grab journals, but be prepared to jump a period back to get the right ones: + Log::info('Now at no-cat loop start.'); + while ($count === 0 && $loop < 3) { + $loop++; + Log::info('Count is zero, search for journals.'); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withoutCategory()->withOpposingAccount(); + $collector->disableInternalFilter(); + $journals = $collector->getPaginatedJournals(); + $journals->setPath('/categories/list/no-category'); + $count = $journals->getCollection()->count(); + if ($count === 0) { + $start->subDay(); + $start = Navigation::startOfPeriod($start, $range); + $end = Navigation::endOfPeriod($start, $range); + Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d'))); + } + } + + // fix title: + if ((strlen($moment) > 0 && $moment !== 'all') || strlen($moment) === 0) { + $subTitle = trans( + 'firefly.without_category_between', + ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] + ); + } + + return view('categories.no-category', compact('journals', 'subTitle', 'moment', 'periods', 'start', 'end')); } /** @@ -356,4 +414,78 @@ class CategoryController extends Controller return $entries; } + + /** + * @return Collection + */ + private function noCategoryPeriodEntries(): Collection + { + $repository = app(JournalRepositoryInterface::class); + $first = $repository->first(); + $start = $first->date ?? new Carbon; + $range = Preferences::get('viewRange', '1M')->data; + $start = Navigation::startOfPeriod($start, $range); + $end = Navigation::endOfX(new Carbon, $range); + $entries = new Collection; + + // properties for cache + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('no-budget-period-entries'); + + if ($cache->has()) { + return $cache->get(); // @codeCoverageIgnore + } + + Log::debug('Going to get period expenses and incomes.'); + while ($end >= $start) { + $end = Navigation::startOfPeriod($end, $range); + $currentEnd = Navigation::endOfPeriod($end, $range); + + // count journals without budget in this period: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withoutCategory()->withOpposingAccount(); + $count = $collector->getJournals()->count(); + + // amount transferred + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withoutCategory() + ->withOpposingAccount()->setTypes([TransactionType::TRANSFER])->disableInternalFilter(); + $transferred = Steam::positive($collector->getJournals()->sum('transaction_amount')); + + // amount spent + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withoutCategory()->withOpposingAccount()->setTypes([TransactionType::WITHDRAWAL]); + $spent = $collector->getJournals()->sum('transaction_amount'); + + // amount earned + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withoutCategory()->withOpposingAccount()->setTypes([TransactionType::DEPOSIT]); + $earned = $collector->getJournals()->sum('transaction_amount'); + + $dateStr = $end->format('Y-m-d'); + $dateName = Navigation::periodShow($end, $range); + $entries->push( + [ + 'string' => $dateStr, + 'name' => $dateName, + 'count' => $count, + 'spent' => $spent, + 'earned' => $earned, + 'transferred' => $transferred, + 'date' => clone $end, + ] + ); + $end = Navigation::subtractPeriod($end, $range, 1); + } + $cache->store($entries); + + return $entries; + } + } diff --git a/app/Http/breadcrumbs.php b/app/Http/breadcrumbs.php index 6ef2d16b4b..247eb5f482 100644 --- a/app/Http/breadcrumbs.php +++ b/app/Http/breadcrumbs.php @@ -354,12 +354,27 @@ Breadcrumbs::register( ); Breadcrumbs::register( - 'categories.no-category', function (BreadCrumbGenerator $breadcrumbs, $subTitle) { + 'categories.no-category', function (BreadCrumbGenerator $breadcrumbs, string $moment, Carbon $start, Carbon $end) { $breadcrumbs->parent('categories.index'); - $breadcrumbs->push($subTitle, route('categories.no-category')); + $breadcrumbs->push(trans('firefly.journals_without_category'), route('categories.no-category')); + + // push when is all: + if ($moment === 'all') { + $breadcrumbs->push(trans('firefly.all_journals_without_category'), route('categories.no-category', ['all'])); + } + // when is specific period: + if (strlen($moment) > 0 && $moment !== 'all') { + $title = trans('firefly.without_category_between', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))), + 'end' => $end->formatLocalized(strval(trans('config.month_and_day')))] + ); + $breadcrumbs->push($title, route('categories.no-category', [$moment])); + } + + } ); + /** * CURRENCIES */ diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index cbf0516c67..972214df99 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -116,9 +116,12 @@ return [ 'multi_select_all_selected' => 'All selected', 'multi_select_filter_placeholder' => 'Find..', 'all_journals_without_budget' => 'All transactions without a budget', + 'all_journals_without_category' => 'All transactions without a category', 'journals_without_budget' => 'Transactions without a budget', - 'all_journals_for_account' => 'All transactions for account :name', - 'journals_in_period_for_account' => 'All transactions for account :name between :start and :end', + 'journals_without_category' => 'Transactions without a category', + 'all_journals_for_account' => 'All transactions for account :name', + 'journals_in_period_for_account' => 'All transactions for account :name between :start and :end', + 'transferred' => 'Transferred', // repeat frequencies: diff --git a/resources/views/budgets/no-budget.twig b/resources/views/budgets/no-budget.twig index 1b169a6270..66b6dbec6a 100644 --- a/resources/views/budgets/no-budget.twig +++ b/resources/views/budgets/no-budget.twig @@ -22,7 +22,7 @@

{{ subTitle }}

- {% include 'list.journals-tasker' with {'journals': journals} %} + {% include 'list.journals-tasker' with {'journals': journals,'hideBudgets': true} %} {% if periods.count > 0 %}

@@ -53,7 +53,7 @@ {{ entry.count }} - {{ 'sum'|_ }} + {{ 'spent'|_ }} {{ entry.sum|formatAmount }} diff --git a/resources/views/categories/no-category.twig b/resources/views/categories/no-category.twig index c302ca0203..f6660a08b7 100644 --- a/resources/views/categories/no-category.twig +++ b/resources/views/categories/no-category.twig @@ -1,23 +1,86 @@ {% extends "./layout/default" %} {% block breadcrumbs %} - {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, subTitle) }} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, moment, start, end) }} {% endblock %} {% block content %} + + {# upper show-all instruction #} + {% if periods.count > 0 %} +

+ +
+ {% endif %} +
-
+
- {{ subTitle }} +

{{ subTitle }}

-
- {% include 'list.journals-tasker' %} +
+ {% include 'list.journals-tasker' with {'journals': journals, 'hideCategories':true} %} + {% if periods.count > 0 %} +

+ + {{ 'show_all_no_filter'|_ }} +

+ {% else %} +

+ + {{ 'show_the_current_period_and_overview'|_ }} +

+ {% endif %}
+ + {% if periods.count > 0 %} +
+ {% for entry in periods %} +
+ +
+ + + + + + + + + + + + + + + + + +
{{ 'transactions'|_ }}{{ entry.count }}
{{ 'spent'|_ }}{{ entry.spent|formatAmount }}
{{ 'earned'|_ }}{{ entry.earned|formatAmount }}
{{ 'transferred'|_ }}{{ entry.transferred|formatAmountPlain }}
+
+
+ {% endfor %} +
+ {% endif %} +
+ {# lower show-all instruction #} + {% if periods.count > 0 %} + + {% endif %} {% endblock %} {% block scripts %} diff --git a/routes/web.php b/routes/web.php index 6abaca3534..cdfeb2ca58 100755 --- a/routes/web.php +++ b/routes/web.php @@ -165,7 +165,7 @@ Route::group( Route::get('show/{category}', ['uses' => 'CategoryController@show', 'as' => 'show']); Route::get('show/{category}/all', ['uses' => 'CategoryController@showAll', 'as' => 'show.all']); Route::get('show/{category}/{date}', ['uses' => 'CategoryController@showByDate', 'as' => 'show.date']); - Route::get('list/no-category', ['uses' => 'CategoryController@noCategory', 'as' => 'no-category']); + Route::get('list/no-category/{moment?}', ['uses' => 'CategoryController@noCategory', 'as' => 'no-category']); Route::post('store', ['uses' => 'CategoryController@store', 'as' => 'store']); Route::post('update/{category}', ['uses' => 'CategoryController@update', 'as' => 'update']); diff --git a/tests/Feature/Controllers/CategoryControllerTest.php b/tests/Feature/Controllers/CategoryControllerTest.php index c66c7a1a7c..5c4ed07c85 100644 --- a/tests/Feature/Controllers/CategoryControllerTest.php +++ b/tests/Feature/Controllers/CategoryControllerTest.php @@ -123,12 +123,20 @@ class CategoryControllerTest extends TestCase // mock stuff $collector = $this->mock(JournalCollectorInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); - $journalRepos->shouldReceive('first')->once()->andReturn(new TransactionJournal); + $journalRepos->shouldReceive('first')->twice()->andReturn(new TransactionJournal); $collector->shouldReceive('setAllAssetAccounts')->andReturnSelf(); + $collector->shouldReceive('setTypes')->andReturnSelf(); $collector->shouldReceive('setRange')->andReturnSelf(); + $collector->shouldReceive('withOpposingAccount')->andReturnSelf(); $collector->shouldReceive('withoutCategory')->andReturnSelf(); $collector->shouldReceive('getJournals')->andReturn(new Collection); + $collector->shouldReceive('getPaginatedJournals')->andReturn(new LengthAwarePaginator([], 0, 10)); + + $collector->shouldReceive('setPage')->andReturnSelf(); + $collector->shouldReceive('disableInternalFilter')->andReturnSelf(); + $collector->shouldReceive('setLimit')->andReturnSelf(); + $this->be($this->user()); $this->changeDateRange($this->user(), $range); @@ -138,6 +146,72 @@ class CategoryControllerTest extends TestCase $response->assertSee('