Added random shift button to shifts view
This commit is contained in:
parent
dc7c62ffe5
commit
14dbe7f5d9
|
@ -71,6 +71,11 @@ $route->addGroup('/angeltypes', function (RouteCollector $route): void {
|
||||||
$route->get('/about', 'AngelTypesController@about');
|
$route->get('/about', 'AngelTypesController@about');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Shifts
|
||||||
|
$route->addGroup('/shifts', function (RouteCollector $route): void {
|
||||||
|
$route->get('/random', 'ShiftsController@random');
|
||||||
|
});
|
||||||
|
|
||||||
// News
|
// News
|
||||||
$route->get('/meetings', 'NewsController@meetings');
|
$route->get('/meetings', 'NewsController@meetings');
|
||||||
$route->addGroup(
|
$route->addGroup(
|
||||||
|
|
|
@ -281,7 +281,9 @@ function view_user_shifts()
|
||||||
$end_day = $shiftsFilter->getEnd()->format('Y-m-d');
|
$end_day = $shiftsFilter->getEnd()->format('Y-m-d');
|
||||||
$end_time = $shiftsFilter->getEnd()->format('H:i');
|
$end_time = $shiftsFilter->getEnd()->format('H:i');
|
||||||
|
|
||||||
|
$canSignUpForShifts = true;
|
||||||
if (config('signup_requires_arrival') && !$user->state->arrived) {
|
if (config('signup_requires_arrival') && !$user->state->arrived) {
|
||||||
|
$canSignUpForShifts = false;
|
||||||
info(render_user_arrived_hint((bool) $user->state->user_info));
|
info(render_user_arrived_hint((bool) $user->state->user_info));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,7 +346,11 @@ function view_user_shifts()
|
||||||
'set_last_4h' => __('last 4h'),
|
'set_last_4h' => __('last 4h'),
|
||||||
'set_next_4h' => __('next 4h'),
|
'set_next_4h' => __('next 4h'),
|
||||||
'set_next_8h' => __('next 8h'),
|
'set_next_8h' => __('next 8h'),
|
||||||
'buttons' => button(
|
'random' => auth()->can('user_shifts') && $canSignUpForShifts ? button(
|
||||||
|
url('/shifts/random'),
|
||||||
|
icon('dice-4-fill') . __('shifts.random')
|
||||||
|
) : '',
|
||||||
|
'dashboard' => button(
|
||||||
public_dashboard_link(),
|
public_dashboard_link(),
|
||||||
icon('speedometer2') . __('Public Dashboard')
|
icon('speedometer2') . __('Public Dashboard')
|
||||||
),
|
),
|
||||||
|
|
|
@ -254,6 +254,9 @@ msgstr ""
|
||||||
"Da die gelöschte Schicht bereits vergangen ist, "
|
"Da die gelöschte Schicht bereits vergangen ist, "
|
||||||
"haben wir einen entsprechenden Arbeitseinsatz hinzugefügt."
|
"haben wir einen entsprechenden Arbeitseinsatz hinzugefügt."
|
||||||
|
|
||||||
|
msgid "notification.shift.no_next_found"
|
||||||
|
msgstr "Es wurde keine verfügbare Schicht gefunden."
|
||||||
|
|
||||||
msgid "user.edit.success"
|
msgid "user.edit.success"
|
||||||
msgstr "Benutzer erfolgreich bearbeitet."
|
msgstr "Benutzer erfolgreich bearbeitet."
|
||||||
|
|
||||||
|
|
|
@ -222,6 +222,9 @@ msgstr "Das Aufbau Start Datum muss vor dem Abbau Ende Datum liegen."
|
||||||
msgid "Public Dashboard"
|
msgid "Public Dashboard"
|
||||||
msgstr "Öffentliches Dashboard"
|
msgstr "Öffentliches Dashboard"
|
||||||
|
|
||||||
|
msgid "shifts.random"
|
||||||
|
msgstr "Zufällige Schicht"
|
||||||
|
|
||||||
msgid "%s has been subscribed to the shift."
|
msgid "%s has been subscribed to the shift."
|
||||||
msgstr "%s wurde in die Schicht eingetragen."
|
msgstr "%s wurde in die Schicht eingetragen."
|
||||||
|
|
||||||
|
|
|
@ -253,6 +253,9 @@ msgstr ""
|
||||||
"Since the deleted shift was already done, "
|
"Since the deleted shift was already done, "
|
||||||
"we added a worklog entry instead, to keep your work hours correct."
|
"we added a worklog entry instead, to keep your work hours correct."
|
||||||
|
|
||||||
|
msgid "notification.shift.no_next_found"
|
||||||
|
msgstr "There is no available shift."
|
||||||
|
|
||||||
msgid "user.edit.success"
|
msgid "user.edit.success"
|
||||||
msgstr "User edited successfully."
|
msgstr "User edited successfully."
|
||||||
|
|
||||||
|
|
|
@ -749,6 +749,9 @@ msgstr "Count"
|
||||||
msgid "general.created_at"
|
msgid "general.created_at"
|
||||||
msgstr "Created at"
|
msgstr "Created at"
|
||||||
|
|
||||||
|
msgid "shifts.random"
|
||||||
|
msgstr "Zufällige Schicht"
|
||||||
|
|
||||||
msgid "shifts.history"
|
msgid "shifts.history"
|
||||||
msgstr "Shifts history"
|
msgstr "Shifts history"
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,10 @@
|
||||||
<button type="button" class="btn btn-secondary set-time" data-hours="8">%set_next_8h%</button>
|
<button type="button" class="btn btn-secondary set-time" data-hours="8">%set_next_8h%</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group mb-1" role="group">
|
<div class="btn-group mb-1" role="group">
|
||||||
%buttons%
|
%random%
|
||||||
|
</div>
|
||||||
|
<div class="btn-group mb-1" role="group">
|
||||||
|
%dashboard%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row d-print-none">
|
<div class="row d-print-none">
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Engelsystem\Controllers;
|
||||||
|
|
||||||
|
use Engelsystem\Helpers\Carbon;
|
||||||
|
use Engelsystem\Http\Redirector;
|
||||||
|
use Engelsystem\Http\Response;
|
||||||
|
use Engelsystem\Helpers\Authenticator;
|
||||||
|
use Engelsystem\Http\UrlGeneratorInterface;
|
||||||
|
use Engelsystem\Models\Shifts\Shift;
|
||||||
|
use Engelsystem\Models\User\User;
|
||||||
|
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||||
|
use Illuminate\Database\Eloquent\Collection as DbCollection;
|
||||||
|
use Illuminate\Database\Query\JoinClause;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Database\Query\Builder;
|
||||||
|
|
||||||
|
class ShiftsController extends BaseController
|
||||||
|
{
|
||||||
|
use HasUserNotifications;
|
||||||
|
|
||||||
|
/** @var string[] */
|
||||||
|
protected array $permissions = [
|
||||||
|
'user_shifts',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected Authenticator $auth,
|
||||||
|
protected Redirector $redirect,
|
||||||
|
protected UrlGeneratorInterface $url,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function random(): Response
|
||||||
|
{
|
||||||
|
$user = $this->auth->user();
|
||||||
|
$nextFreeShifts = $this->getNextFreeShifts($user);
|
||||||
|
|
||||||
|
if ($nextFreeShifts->isEmpty()) {
|
||||||
|
$this->addNotification('notification.shift.no_next_found', NotificationType::WARNING);
|
||||||
|
return $this->redirect->to($this->url->to('/shifts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Shift $randomShift */
|
||||||
|
$randomShift = $nextFreeShifts
|
||||||
|
->collect()
|
||||||
|
// Prefer soon starting shifts
|
||||||
|
->groupBy('start')
|
||||||
|
->first()
|
||||||
|
// Select one at random
|
||||||
|
->random();
|
||||||
|
|
||||||
|
return $this->redirect->to($this->url->to('/shifts', ['action' => 'view', 'shift_id' => $randomShift->id]));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getNextFreeShifts(User $user): Collection | DbCollection
|
||||||
|
{
|
||||||
|
$angelTypes = $user
|
||||||
|
->userAngelTypes()
|
||||||
|
->select('angel_types.id')
|
||||||
|
->whereNested(function (Builder $query): void {
|
||||||
|
$query
|
||||||
|
->where('angel_types.restricted', false)
|
||||||
|
->orWhereNot('confirm_user_id', false);
|
||||||
|
});
|
||||||
|
$shifts = $user->shiftEntries()->select('shift_id');
|
||||||
|
|
||||||
|
$freeShifts = Shift::query()
|
||||||
|
->select('shifts.*')
|
||||||
|
// Load needed from shift if no schedule configured, else from room
|
||||||
|
->leftJoin('schedule_shift', 'schedule_shift.shift_id', '=', 'shifts.id')
|
||||||
|
->leftJoin('needed_angel_types', function (JoinClause $query): void {
|
||||||
|
$query->on('needed_angel_types.shift_id', '=', 'shifts.id')
|
||||||
|
->whereNull('schedule_shift.shift_id');
|
||||||
|
})
|
||||||
|
->leftJoin('needed_angel_types AS nas', function (JoinClause $query): void {
|
||||||
|
$query->on('nas.location_id', '=', 'shifts.location_id')
|
||||||
|
->whereNotNull('schedule_shift.shift_id');
|
||||||
|
})
|
||||||
|
// Not already signed in
|
||||||
|
->whereNotIn('shifts.id', $shifts)
|
||||||
|
// Same angel types
|
||||||
|
->where(function (EloquentBuilder $query) use ($angelTypes): void {
|
||||||
|
$query
|
||||||
|
->whereIn('needed_angel_types.angel_type_id', $angelTypes)
|
||||||
|
->orWhereIn('nas.angel_type_id', $angelTypes);
|
||||||
|
})
|
||||||
|
// Starts soon
|
||||||
|
->where('shifts.start', '>', Carbon::now())
|
||||||
|
// Where help needed
|
||||||
|
->where(function (Builder $query): void {
|
||||||
|
$query
|
||||||
|
->from('shift_entries')
|
||||||
|
->selectRaw('COUNT(*)')
|
||||||
|
->where(fn(Builder $query) => $this->queryShiftEntries($query));
|
||||||
|
}, '<', Shift::query()->raw('COALESCE(needed_angel_types.count, nas.count)'))
|
||||||
|
->limit(10)
|
||||||
|
->orderBy('start');
|
||||||
|
|
||||||
|
return $freeShifts->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function queryShiftEntries(Builder $query): void
|
||||||
|
{
|
||||||
|
$query->select('id')
|
||||||
|
->from('shift_entries')
|
||||||
|
->where(function (Builder $query): void {
|
||||||
|
$query->where('shift_entries.shift_id', $query->raw('needed_angel_types.shift_id'))
|
||||||
|
->where('shift_entries.angel_type_id', $query->raw('needed_angel_types.angel_type_id'));
|
||||||
|
})
|
||||||
|
->orWhere(function (Builder $query): void {
|
||||||
|
$query->where('shift_entries.shift_id', $query->raw('nas.shift_id'))
|
||||||
|
->where('shift_entries.angel_type_id', $query->raw('nas.angel_type_id'));
|
||||||
|
})
|
||||||
|
->groupBy(['shift_entries.shift_id', 'shift_entries.angel_type_id']);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Engelsystem\Test\Unit\Controllers;
|
||||||
|
|
||||||
|
use Engelsystem\Controllers\NotificationType;
|
||||||
|
use Engelsystem\Controllers\ShiftsController;
|
||||||
|
use Engelsystem\Helpers\Authenticator;
|
||||||
|
use Engelsystem\Helpers\Carbon;
|
||||||
|
use Engelsystem\Http\Redirector;
|
||||||
|
use Engelsystem\Http\UrlGeneratorInterface;
|
||||||
|
use Engelsystem\Models\AngelType;
|
||||||
|
use Engelsystem\Models\Shifts\NeededAngelType;
|
||||||
|
use Engelsystem\Models\Shifts\Shift;
|
||||||
|
use Engelsystem\Models\Shifts\ShiftEntry;
|
||||||
|
use Engelsystem\Models\User\User;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
|
||||||
|
class ShiftsControllerTest extends ControllerTest
|
||||||
|
{
|
||||||
|
protected Authenticator|MockObject $auth;
|
||||||
|
protected Redirector|MockObject $redirect;
|
||||||
|
protected UrlGeneratorInterface $url;
|
||||||
|
protected User $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \Engelsystem\Controllers\ShiftsController::random
|
||||||
|
* @covers \Engelsystem\Controllers\ShiftsController::__construct
|
||||||
|
*/
|
||||||
|
public function testRandomNonShiftsFound(): void
|
||||||
|
{
|
||||||
|
$this->createModels();
|
||||||
|
|
||||||
|
$this->setExpects($this->redirect, 'to', ['http://localhost/shifts'], $this->response);
|
||||||
|
$this->setExpects($this->auth, 'user', null, $this->user);
|
||||||
|
|
||||||
|
$controller = new ShiftsController($this->auth, $this->redirect, $this->url);
|
||||||
|
|
||||||
|
$return = $controller->random();
|
||||||
|
$this->assertEquals($this->response, $return);
|
||||||
|
$this->assertHasNotification('notification.shift.no_next_found', NotificationType::WARNING);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \Engelsystem\Controllers\ShiftsController::random
|
||||||
|
* @covers \Engelsystem\Controllers\ShiftsController::getNextFreeShifts
|
||||||
|
* @covers \Engelsystem\Controllers\ShiftsController::queryShiftEntries
|
||||||
|
*/
|
||||||
|
public function testRandom(): void
|
||||||
|
{
|
||||||
|
$this->createModels();
|
||||||
|
$this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce());
|
||||||
|
$start = Carbon::now()->addHour();
|
||||||
|
|
||||||
|
$otherUser = User::factory()->create();
|
||||||
|
[$userAngelType, $otherAngelType] = AngelType::factory(2)->create();
|
||||||
|
[$possibleShift1, $possibleShift2, $otherAngelTypeShift, $alreadySubscribedShift] = Shift::factory(4)
|
||||||
|
->create(['start' => $start]);
|
||||||
|
$this->user->userAngelTypes()->attach($userAngelType, ['confirm_user_id' => $this->user->id]);
|
||||||
|
NeededAngelType::factory()->create([
|
||||||
|
'shift_id' => $possibleShift1->id,
|
||||||
|
'angel_type_id' => $userAngelType->id,
|
||||||
|
'count' => 2,
|
||||||
|
]);
|
||||||
|
NeededAngelType::factory()->create([
|
||||||
|
'shift_id' => $possibleShift2->id,
|
||||||
|
'angel_type_id' => $userAngelType->id,
|
||||||
|
'count' => 1,
|
||||||
|
]);
|
||||||
|
NeededAngelType::factory()->create([
|
||||||
|
'shift_id' => $otherAngelTypeShift->id,
|
||||||
|
'angel_type_id' => $otherAngelType->id,
|
||||||
|
'count' => 3,
|
||||||
|
]);
|
||||||
|
ShiftEntry::factory()->create([
|
||||||
|
'shift_id' => $alreadySubscribedShift->id,
|
||||||
|
'angel_type_id' => $userAngelType->id,
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$otherUser->userAngelTypes()->attach($userAngelType, ['confirm_user_id' => $otherUser->id]);
|
||||||
|
ShiftEntry::factory()->create([
|
||||||
|
'shift_id' => $possibleShift1->id,
|
||||||
|
'angel_type_id' => $userAngelType->id,
|
||||||
|
'user_id' => $otherUser,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->redirect->expects($this->exactly(10))
|
||||||
|
->method('to')
|
||||||
|
->willReturnCallback(function (string $url) use ($possibleShift1, $possibleShift2) {
|
||||||
|
parse_str(parse_url($url)['query'], $parameters);
|
||||||
|
$this->assertTrue(Str::startsWith($url, 'http://localhost/shifts'));
|
||||||
|
$this->assertArrayHasKey('shift_id', $parameters);
|
||||||
|
$shiftId = $parameters['shift_id'];
|
||||||
|
$this->assertTrue(in_array($shiftId, [$possibleShift1->id, $possibleShift2->id]));
|
||||||
|
return $this->response;
|
||||||
|
});
|
||||||
|
|
||||||
|
$controller = new ShiftsController($this->auth, $this->redirect, $this->url);
|
||||||
|
|
||||||
|
$return = $controller->random();
|
||||||
|
$this->assertEquals($this->response, $return);
|
||||||
|
|
||||||
|
// Try multiple times
|
||||||
|
for ($i = 1; $i < 10; $i++) {
|
||||||
|
$controller->random();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createModels(): void
|
||||||
|
{
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->auth = $this->createMock(Authenticator::class);
|
||||||
|
|
||||||
|
$this->redirect = $this->createMock(Redirector::class);
|
||||||
|
|
||||||
|
$this->url = $this->app->make(UrlGeneratorInterface::class);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue