Also implement #595 for the no-cat view.

This commit is contained in:
James Cole
2017-03-10 19:34:46 +01:00
parent 0c52d54d7d
commit ecbd7ca95b
7 changed files with 315 additions and 28 deletions

View File

@@ -18,13 +18,17 @@ use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Http\Requests\CategoryFormRequest; use FireflyIII\Http\Requests\CategoryFormRequest;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Models\Category; use FireflyIII\Models\Category;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Support\CacheProperties; use FireflyIII\Support\CacheProperties;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Log;
use Navigation; use Navigation;
use Preferences; use Preferences;
use Steam;
use View; use View;
/** /**
@@ -152,24 +156,78 @@ class CategoryController extends Controller
/** /**
* @return View * @return View
*/ */
public function noCategory() public function noCategory(Request $request, JournalRepositoryInterface $repository, string $moment = '')
{ {
/** @var Carbon $start */ // default values:
$start = session('start', Carbon::now()->startOfMonth()); $range = Preferences::get('viewRange', '1M')->data;
/** @var Carbon $end */ $start = null;
$end = session('end', Carbon::now()->startOfMonth()); $end = null;
$periods = new Collection;
// new collector: // prep for "all" view.
/** @var JournalCollectorInterface $collector */ if ($moment === 'all') {
$collector = app(JournalCollectorInterface::class); $subTitle = trans('firefly.all_journals_without_category');
$collector->setAllAssetAccounts()->setRange($start, $end)->withoutCategory();//->groupJournals(); $first = $repository->first();
$journals = $collector->getJournals(); $start = $first->date ?? new Carbon;
$subTitle = trans( $end = new Carbon;
'firefly.without_category_between', }
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
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 $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;
}
} }

View File

@@ -354,12 +354,27 @@ Breadcrumbs::register(
); );
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->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 * CURRENCIES
*/ */

View File

@@ -116,9 +116,12 @@ return [
'multi_select_all_selected' => 'All selected', 'multi_select_all_selected' => 'All selected',
'multi_select_filter_placeholder' => 'Find..', 'multi_select_filter_placeholder' => 'Find..',
'all_journals_without_budget' => 'All transactions without a budget', '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', 'journals_without_budget' => 'Transactions without a budget',
'all_journals_for_account' => 'All transactions for account :name', 'journals_without_category' => 'Transactions without a category',
'journals_in_period_for_account' => 'All transactions for account :name between :start and :end', '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: // repeat frequencies:

View File

@@ -22,7 +22,7 @@
<h3 class="box-title">{{ subTitle }}</h3> <h3 class="box-title">{{ subTitle }}</h3>
</div> </div>
<div class="box-body "> <div class="box-body ">
{% include 'list.journals-tasker' with {'journals': journals} %} {% include 'list.journals-tasker' with {'journals': journals,'hideBudgets': true} %}
{% if periods.count > 0 %} {% if periods.count > 0 %}
<p> <p>
<i class="fa fa-calendar" aria-hidden="true"></i> <i class="fa fa-calendar" aria-hidden="true"></i>
@@ -53,7 +53,7 @@
<td style="text-align: right;">{{ entry.count }}</td> <td style="text-align: right;">{{ entry.count }}</td>
</tr> </tr>
<tr> <tr>
<td style="width:33%;">{{ 'sum'|_ }}</td> <td style="width:33%;">{{ 'spent'|_ }}</td>
<td style="text-align: right;">{{ entry.sum|formatAmount }}</td> <td style="text-align: right;">{{ entry.sum|formatAmount }}</td>
</tr> </tr>
</table> </table>

View File

@@ -1,23 +1,86 @@
{% extends "./layout/default" %} {% extends "./layout/default" %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, subTitle) }} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, moment, start, end) }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{# upper show-all instruction #}
{% if periods.count > 0 %}
<div class="row">
<div class="col-lg-offset-10 col-lg-2 col-md-offset-10 col-md-2 col-sm-12 col-xs-12">
<p class="small text-center"><a href="{{ route('categories.no-category',['all']) }}">{{ 'showEverything'|_ }}</a></p>
</div>
</div>
{% endif %}
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12"> <div class="{% if periods.count > 0 %}col-lg-10 col-md-10 col-sm-12{% else %}col-lg-12 col-md-12 col-sm-12{% endif %}">
<div class="box"> <div class="box">
<div class="box-header with-border"> <div class="box-header with-border">
{{ subTitle }} <h3 class="box-title">{{ subTitle }}</h3>
</div> </div>
<div class="box-body"> <div class="box-body ">
{% include 'list.journals-tasker' %} {% include 'list.journals-tasker' with {'journals': journals, 'hideCategories':true} %}
{% if periods.count > 0 %}
<p>
<i class="fa fa-calendar" aria-hidden="true"></i>
<a href="{{ route('categories.no-category', ['all']) }}">{{ 'show_all_no_filter'|_ }}</a>
</p>
{% else %}
<p>
<i class="fa fa-calendar" aria-hidden="true"></i>
<a href="{{ route('categories.no-category') }}">{{ 'show_the_current_period_and_overview'|_ }}</a>
</p>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
{% if periods.count > 0 %}
<div class="col-lg-2 col-md-2 col-sm-12 col-xs-12">
{% for entry in periods %}
<div class="box {% if entry.date == start %}box-solid box-primary{% endif %}">
<div class="box-header with-border">
<h3 class="box-title"><a href="{{ route('categories.no-category',[entry.string]) }}">{{ entry.name }}</a>
</h3>
</div>
<div class="box-body no-padding">
<table class="table table-hover">
<tr>
<td style="width:33%;">{{ 'transactions'|_ }}</td>
<td style="text-align: right;">{{ entry.count }}</td>
</tr>
<tr>
<td style="width:33%;">{{ 'spent'|_ }}</td>
<td style="text-align: right;">{{ entry.spent|formatAmount }}</td>
</tr>
<tr>
<td style="width:33%;">{{ 'earned'|_ }}</td>
<td style="text-align: right;">{{ entry.earned|formatAmount }}</td>
</tr>
<tr>
<td style="width:33%;">{{ 'transferred'|_ }}</td>
<td style="text-align: right;" class="text-info">{{ entry.transferred|formatAmountPlain }}</td>
</tr>
</table>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div> </div>
{# lower show-all instruction #}
{% if periods.count > 0 %}
<div class="row">
<div class="col-lg-offset-10 col-lg-2 col-md-offset-10 col-md-2 col-sm-12 col-xs-12">
<p class="small text-center"><a href="{{ route('categories.no-category',['all']) }}">{{ 'showEverything'|_ }}</a></p>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}

View File

@@ -165,7 +165,7 @@ Route::group(
Route::get('show/{category}', ['uses' => 'CategoryController@show', 'as' => 'show']); Route::get('show/{category}', ['uses' => 'CategoryController@show', 'as' => 'show']);
Route::get('show/{category}/all', ['uses' => 'CategoryController@showAll', 'as' => 'show.all']); Route::get('show/{category}/all', ['uses' => 'CategoryController@showAll', 'as' => 'show.all']);
Route::get('show/{category}/{date}', ['uses' => 'CategoryController@showByDate', 'as' => 'show.date']); 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('store', ['uses' => 'CategoryController@store', 'as' => 'store']);
Route::post('update/{category}', ['uses' => 'CategoryController@update', 'as' => 'update']); Route::post('update/{category}', ['uses' => 'CategoryController@update', 'as' => 'update']);

View File

@@ -123,12 +123,20 @@ class CategoryControllerTest extends TestCase
// mock stuff // mock stuff
$collector = $this->mock(JournalCollectorInterface::class); $collector = $this->mock(JournalCollectorInterface::class);
$journalRepos = $this->mock(JournalRepositoryInterface::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('setAllAssetAccounts')->andReturnSelf();
$collector->shouldReceive('setTypes')->andReturnSelf();
$collector->shouldReceive('setRange')->andReturnSelf(); $collector->shouldReceive('setRange')->andReturnSelf();
$collector->shouldReceive('withOpposingAccount')->andReturnSelf();
$collector->shouldReceive('withoutCategory')->andReturnSelf(); $collector->shouldReceive('withoutCategory')->andReturnSelf();
$collector->shouldReceive('getJournals')->andReturn(new Collection); $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->be($this->user());
$this->changeDateRange($this->user(), $range); $this->changeDateRange($this->user(), $range);
@@ -138,6 +146,72 @@ class CategoryControllerTest extends TestCase
$response->assertSee('<ol class="breadcrumb">'); $response->assertSee('<ol class="breadcrumb">');
} }
/**
* @covers \FireflyIII\Http\Controllers\CategoryController::noCategory
* @dataProvider dateRangeProvider
*
* @param string $range
*/
public function testNoCategoryAll(string $range)
{
// mock stuff
$collector = $this->mock(JournalCollectorInterface::class);
$journalRepos = $this->mock(JournalRepositoryInterface::class);
$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);
$response = $this->get(route('categories.no-category', ['all']));
$response->assertStatus(200);
// has bread crumb
$response->assertSee('<ol class="breadcrumb">');
}
/**
* @covers \FireflyIII\Http\Controllers\CategoryController::noCategory
* @dataProvider dateRangeProvider
*
* @param string $range
*/
public function testNoCategoryDate(string $range)
{
// mock stuff
$collector = $this->mock(JournalCollectorInterface::class);
$journalRepos = $this->mock(JournalRepositoryInterface::class);
$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);
$response = $this->get(route('categories.no-category', ['2016-01-01']));
$response->assertStatus(200);
// has bread crumb
$response->assertSee('<ol class="breadcrumb">');
}
/** /**
* @covers \FireflyIII\Http\Controllers\CategoryController::show * @covers \FireflyIII\Http\Controllers\CategoryController::show
* @covers \FireflyIII\Http\Controllers\CategoryController::getGroupedEntries * @covers \FireflyIII\Http\Controllers\CategoryController::getGroupedEntries