Recreated shifts history page

This commit is contained in:
Igor Scheller 2023-10-22 18:37:29 +02:00 committed by msquare
parent ac74ab489d
commit bf83e6a300
15 changed files with 329 additions and 145 deletions

View File

@ -153,6 +153,15 @@ $route->addGroup(
}
);
// Shifts
$route->addGroup(
'/shifts',
function (RouteCollector $route): void {
$route->get('/history', 'Admin\\ShiftsController@history');
$route->post('/history', 'Admin\\ShiftsController@deleteTransaction');
}
);
// Questions
$route->addGroup(
'/questions',

View File

@ -219,7 +219,7 @@ function admin_arrive()
], $planned_arrival_at_day),
table([
'day' => __('Date'),
'count' => __('Count'),
'count' => __('general.count'),
'sum' => __('Sum'),
], $planned_arrival_at_day),
]),
@ -234,7 +234,7 @@ function admin_arrive()
], $arrival_at_day),
table([
'day' => __('Date'),
'count' => __('Count'),
'count' => __('general.count'),
'sum' => __('Sum'),
], $arrival_at_day),
]),
@ -249,7 +249,7 @@ function admin_arrive()
], $planned_departure_at_day),
table([
'day' => __('Date'),
'count' => __('Count'),
'count' => __('general.count'),
'sum' => __('Sum'),
], $planned_departure_at_day),
]),

View File

@ -1,15 +1,11 @@
<?php
use Engelsystem\Database\Db;
use Engelsystem\Helpers\Carbon;
use Engelsystem\Http\Exceptions\HttpForbidden;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\Location;
use Engelsystem\Models\Shifts\NeededAngelType;
use Engelsystem\Models\Shifts\Schedule;
use Engelsystem\Models\Shifts\Shift;
use Engelsystem\Models\Shifts\ShiftType;
use Engelsystem\Models\User\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
@ -457,7 +453,7 @@ function admin_shifts()
return page_with_title(
$link . ' ' . admin_shifts_title() . ' ' . sprintf(
'<a href="%s">%s</a>',
url('/admin-shifts-history'),
url('/admin/shifts/history'),
icon('clock-history')
),
[
@ -480,7 +476,7 @@ function admin_shifts()
div('col-lg-6', [
form_datetime(
'start',
__('Start'),
__('shifts.start'),
$request->has('start')
? Carbon::createFromDatetime($request->input('start'))
: $start
@ -489,7 +485,7 @@ function admin_shifts()
div('col-lg-6', [
form_datetime(
'end',
__('End'),
__('shifts.end'),
$request->has('end')
? Carbon::createFromDatetime($request->input('end'))
: $end
@ -565,99 +561,3 @@ function admin_shifts()
]
);
}
function admin_shifts_history_title(): string
{
return __('Shifts history');
}
/**
* Display shifts transaction history
*
* @return string
*/
function admin_shifts_history(): string
{
if (!auth()->can('admin_shifts')) {
throw new HttpForbidden();
}
$request = request();
$transactionId = $request->postData('transaction_id');
if ($request->hasPostData('delete') && $transactionId) {
$shifts = Shift::whereTransactionId($transactionId)->get();
engelsystem_log('Deleting ' . count($shifts) . ' shifts (transaction id ' . $transactionId . ')');
foreach ($shifts as $shift) {
$shift = Shift($shift);
foreach ($shift->shiftEntries as $entry) {
event('shift.entry.deleting', [
'user' => $entry->user,
'start' => $shift->start,
'end' => $shift->end,
'name' => $shift->shiftType->name,
'title' => $shift->title,
'type' => $entry->angelType->name,
'location' => $shift->location,
'freeloaded' => $entry->freeloaded,
]);
}
$shift->delete();
engelsystem_log(
'Deleted shift ' . $shift->title . ' / ' . $shift->shiftType->name
. ' from ' . $shift->start->format('Y-m-d H:i')
. ' to ' . $shift->end->format('Y-m-d H:i')
);
}
success(sprintf(__('%s shifts deleted.'), count($shifts)));
throw_redirect(url('/admin-shifts-history'));
}
$schedules = Schedule::all()->pluck('name', 'id')->toArray();
$shiftsData = Db::select('
SELECT
s.transaction_id,
s.title,
schedule_shift.schedule_id,
COUNT(s.id) AS count,
MIN(s.start) AS start,
MAX(s.end) AS end,
s.created_by AS user_id,
MAX(s.created_at) AS created_at
FROM shifts AS s
LEFT JOIN schedule_shift on schedule_shift.shift_id = s.id
WHERE s.transaction_id IS NOT NULL
GROUP BY s.transaction_id
ORDER BY created_at DESC
');
foreach ($shiftsData as &$shiftData) {
$shiftData['title'] = $shiftData['schedule_id'] ? __('shifts_history.schedule', [$schedules[$shiftData['schedule_id']]]) : $shiftData['title'];
$shiftData['user'] = User_Nick_render(User::find($shiftData['user_id']));
$shiftData['start'] = Carbon::make($shiftData['start'])->format(__('Y-m-d H:i'));
$shiftData['end'] = Carbon::make($shiftData['end'])->format(__('Y-m-d H:i'));
$shiftData['created_at'] = Carbon::make($shiftData['created_at'])->format(__('Y-m-d H:i'));
$shiftData['actions'] = form([
form_hidden('transaction_id', $shiftData['transaction_id']),
form_submit('delete', icon('trash') . __('delete all'), 'btn-sm', true, 'danger'),
]);
}
return page_with_title(admin_shifts_history_title(), [
msg(),
table([
'transaction_id' => __('ID'),
'title' => __('title.title'),
'count' => __('Count'),
'start' => __('Start'),
'end' => __('End'),
'user' => __('User'),
'created_at' => __('Created'),
'actions' => '',
], $shiftsData),
], true);
}

View File

@ -99,7 +99,7 @@ function ShiftEntry_create_view_admin(
info(__('Do you want to sign up the following user for this shift?'), true),
form([
form_select('angeltype_id', __('Angeltype'), $angeltypes_select, $angeltype->id),
form_select('user_id', __('User'), $users_select, $signup_user->id),
form_select('user_id', __('general.user'), $users_select, $signup_user->id),
form_submit('submit', icon('check-lg') . __('form.save')),
]),
]
@ -134,7 +134,7 @@ function ShiftEntry_create_view_supporter(
$angeltype->name
), true),
form([
form_select('user_id', __('User'), $users_select, $signup_user->id),
form_select('user_id', __('general.user'), $users_select, $signup_user->id),
form_submit('submit', icon('check-lg') . __('form.save')),
]),
]

View File

@ -29,7 +29,7 @@ function Shift_view_header(Shift $shift, Location $location)
. '</p>',
]),
div('col-sm-3 col-xs-6', [
'<h4>' . __('Start') . '</h4>',
'<h4>' . __('shifts.start') . '</h4>',
'<p class="lead' . (time() >= $shift->start->timestamp ? ' text-success' : '') . '">',
icon('calendar-event') . $shift->start->format(__('Y-m-d')),
'<br />',
@ -37,7 +37,7 @@ function Shift_view_header(Shift $shift, Location $location)
'</p>',
]),
div('col-sm-3 col-xs-6', [
'<h4>' . __('End') . '</h4>',
'<h4>' . __('shifts.end') . '</h4>',
'<p class="lead' . (time() >= $shift->end->timestamp ? ' text-success' : '') . '">',
icon('calendar-event') . $shift->end->format(__('Y-m-d')),
'<br />',

View File

@ -153,7 +153,7 @@ function UserAngelType_add_view(AngelType $angeltype, $users_source, $user_id)
form([
form_info(__('Angeltype'), $angeltype->name),
form_checkbox('auto_confirm_user', __('Confirm user'), true),
form_select('user_id', __('User'), $users, $user_id),
form_select('user_id', __('general.user'), $users, $user_id),
form_submit('submit', __('Add')),
]),
]);

View File

@ -280,3 +280,6 @@ msgstr "Die Registrierung ist deaktiviert."
msgid "registration.successful"
msgstr "Registrierung erfolgreich. Du kannst dich jetzt anmelden!"
msgid "shifts.history.delete.success"
msgstr "Schichten erfolgreich gelöscht."

View File

@ -317,18 +317,6 @@ msgstr "Benötigte Engel"
msgid "Shift deleted."
msgstr "Schicht gelöscht."
msgid "Shifts history"
msgstr "Schichten Historie"
msgid "%s shifts deleted."
msgstr "%s Schichten gelöscht."
msgid "Created"
msgstr "Erstellt"
msgid "delete all"
msgstr "alle löschen"
msgid "Do you want to delete the shift %s from %s to %s?"
msgstr "Möchtest Du die Schicht %s von %s bis %s löschen?"
@ -634,9 +622,6 @@ msgstr "Summe angekommen"
msgid "Date"
msgstr "Datum"
msgid "Count"
msgstr "Anzahl"
msgid "Arrival statistics"
msgstr "Ankunfts-Statistik"
@ -673,12 +658,6 @@ msgstr "Gruppe bearbeiten"
msgid "Please select a shift type."
msgstr "Bitte einen Schichttyp wählen."
msgid "Start"
msgstr "Beginn"
msgid "End"
msgstr "Ende"
msgid "Location"
msgstr "Ort"
@ -1112,9 +1091,6 @@ msgstr "Von Schicht austragen"
msgid "Do you want to sign up the following user for this shift?"
msgstr "Möchtest du den folgenden User für die Schicht eintragen?"
msgid "User"
msgstr "Benutzer"
msgid "Do you want to sign up the following user for this shift as %s?"
msgstr "Möchtest du den folgenden User als %s in die Schicht eintragen?"
@ -1542,9 +1518,6 @@ msgstr "Typ"
msgid "schedule.import.shift.location"
msgstr "Ort"
msgid "shifts_history.schedule"
msgstr "Programm: %s"
msgid "news.title"
msgstr "News"
@ -2010,3 +1983,30 @@ msgstr "Registrieren"
msgid "confirmation.delete"
msgstr "Möchtest du es wirklich löschen?"
msgid "general.id"
msgstr "ID"
msgid "general.user"
msgstr "Benutzer"
msgid "general.count"
msgstr "Anzahl"
msgid "general.created_at"
msgstr "Erstellt am"
msgid "shifts.history"
msgstr "Schichten Historie"
msgid "shifts.history.schedule"
msgstr "Programm: %s"
msgid "shifts.history.delete_all.title"
msgstr "%u Schichten löschen"
msgid "shifts.start"
msgstr "Start"
msgid "shifts.end"
msgstr "Ende"

View File

@ -279,3 +279,6 @@ msgstr "The registration is disabled."
msgid "registration.successful"
msgstr "Registration successful. You can now log in!"
msgid "shifts.history.delete.success"
msgstr "Shifts deleted successfully."

View File

@ -157,9 +157,6 @@ msgstr "Title"
msgid "schedule.import.shift.location"
msgstr "Location"
msgid "shifts_history.schedule"
msgstr "Schedule: %s"
msgid "news.title"
msgstr "News"
@ -662,3 +659,30 @@ msgstr "Please enter a mobile number in your settings!"
msgid "confirmation.delete"
msgstr "Do you really want to delete it?"
msgid "general.id"
msgstr "ID"
msgid "general.user"
msgstr "User"
msgid "general.count"
msgstr "Count"
msgid "general.created_at"
msgstr "Created at"
msgid "shifts.history"
msgstr "Shifts history"
msgid "shifts.history.schedule"
msgstr "Schedule: %s"
msgid "shifts.history.delete_all.title"
msgstr "Delete %u shifts"
msgid "shifts.start"
msgstr "Start"
msgid "shifts.end"
msgstr "End"

View File

@ -0,0 +1,71 @@
{% extends 'layouts/app.twig' %}
{% import 'macros/base.twig' as m %}
{% import 'macros/form.twig' as f %}
{% set title %}{% block title %}{{ __('shifts.history') }}{% endblock %}{% endset %}
{% block content %}
<div class="container">
<h1>
{{ m.button(m.icon('chevron-left'), url('/admin-shifts'), null, 'sm') }}
{% block content_title %}{{ title }}{% endblock %}
</h1>
{% include 'layouts/parts/messages.twig' %}
<div class="row">
{% block row_content %}
<div class="col-md-12">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>{{ __('general.id') }}</th>
<th>{{ __('title.title') }}</th>
<th>{{ __('general.count') }}</th>
<th>{{ __('shifts.start') }}</th>
<th>{{ __('shifts.end') }}</th>
<th>{{ __('general.user') }}</th>
<th>{{ __('general.created_at') }}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for shift in shifts %}
<tr>
<td>{{ shift.transaction_id }}</td>
<td>
{% if shift.schedule %}
{{ __('shifts.history.schedule', [shift.schedule.name]) }}
{% else %}
{{ shift.title }}
{% endif %}
</td>
<td>{{ shift.count }}</td>
<td>{{ shift.start.format(__('Y-m-d H:i')) }}</td>
<td>{{ shift.end.format(__('Y-m-d H:i')) }}</td>
<td>{{ m.user(shift.createdBy) }}</td>
<td>{{ shift.created_at.format(__('Y-m-d H:i')) }}</td>
<td>
<form method="post" class="ps-1">
{{ csrf() }}
{{ f.hidden('transaction_id', shift.transaction_id) }}
{{ f.delete(null, {
'size': 'sm',
'title': __('form.delete_all'),
'confirm_title': __('shifts.history.delete_all.title', [shift.count]),
'confirm_button_text': __('form.delete_all'),
}) }}
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -18,7 +18,7 @@
<div class="row g-2">
<div class="col-12">
<div>
<label class="form-label">{{ __('User') }}</label>
<label class="form-label">{{ __('general.user') }}</label>
</div>
{{ m.user(user, {'pronoun': true}) }}
</div>

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Controllers\Admin;
use Engelsystem\Controllers\BaseController;
use Engelsystem\Controllers\HasUserNotifications;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Models\Shifts\Shift;
use Illuminate\Database\Eloquent\Collection;
use Psr\Log\LoggerInterface;
class ShiftsController extends BaseController
{
use HasUserNotifications;
/** @var array<string> */
protected array $permissions = [
'admin_shifts',
];
public function __construct(
protected LoggerInterface $log,
protected Shift $shift,
protected Redirector $redirect,
protected Response $response
) {
}
public function history(): Response
{
$shifts = $this->shift
->select()
->selectRaw('MIN(start) AS start')
->selectRaw('MAX(end) AS end')
->selectRaw('COUNT(*) AS count')
->selectRaw('MIN(created_at) AS created_at')
->with(['schedule', 'createdBy'])
->whereNotNull('transaction_id')
->groupBy('transaction_id')
->orderByDesc('created_at')
->get();
return $this->response->withView('admin/shifts/history', ['shifts' => $shifts]);
}
public function deleteTransaction(Request $request): Response
{
$transactionId = $request->postData('transaction_id');
/** @var Shift[]|Collection $shifts */
$shifts = $this->shift->with([
'location',
'shiftEntries',
'shiftEntries.angelType',
'shiftEntries.user',
'shiftType',
])->where('transaction_id', $transactionId)->get();
$this->log->info(
'Deleting {count} shifts with transaction ID: {id}',
['count' => $shifts->count(), 'id' => $transactionId]
);
foreach ($shifts as $shift) {
foreach ($shift->shiftEntries as $entry) {
event('shift.entry.deleting', [
'user' => $entry->user,
'start' => $shift->start,
'end' => $shift->end,
'name' => $shift->shiftType->name,
'title' => $shift->title,
'type' => $entry->angelType->name,
'location' => $shift->location,
'freeloaded' => $entry->freeloaded,
]);
}
$shift->delete();
$this->log->info(
'Deleted shift ' . $shift->title . ' / ' . $shift->shiftType->name
. ' from ' . $shift->start->format('Y-m-d H:i')
. ' to ' . $shift->end->format('Y-m-d H:i')
);
}
$this->addNotification('shifts.history.delete.success');
return $this->redirect->back();
}
}

View File

@ -24,7 +24,6 @@ class LegacyMiddleware implements MiddlewareInterface
'shift_entries',
'shifts',
'users',
'admin_shifts_history',
];
public function __construct(protected ContainerInterface $container, protected Authenticator $auth)
@ -129,8 +128,6 @@ class LegacyMiddleware implements MiddlewareInterface
$title = admin_shifts_title();
$content = admin_shifts();
return [$title, $content];
case 'admin_shifts_history':
return [admin_shifts_history_title(), admin_shifts_history()];
}
throw_redirect(url('/login'));

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers\Admin;
use Engelsystem\Controllers\Admin\ShiftsController;
use Engelsystem\Events\EventDispatcher;
use Engelsystem\Helpers\Uuid;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Request;
use Engelsystem\Models\Shifts\Shift;
use Engelsystem\Models\Shifts\ShiftEntry;
use Engelsystem\Test\Unit\Controllers\ControllerTest;
use PHPUnit\Framework\MockObject\MockObject;
class ShiftsControllerTest extends ControllerTest
{
protected Redirector|MockObject $redirect;
/**
* @covers \Engelsystem\Controllers\Admin\ShiftsController::__construct
* @covers \Engelsystem\Controllers\Admin\ShiftsController::history
*/
public function testHistory(): void
{
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$this->assertEquals('admin/shifts/history', $view);
$this->assertCount(2, $data['shifts'] ?? []);
return $this->response;
});
/** @var ShiftsController $controller */
$controller = $this->app->make(ShiftsController::class);
$controller->history();
}
/**
* @covers \Engelsystem\Controllers\Admin\ShiftsController::deleteTransaction
*/
public function testDeleteTransaction(): void
{
$this->database->getConnection()->getRawPdo()->exec('PRAGMA foreign_keys = ON');
$this->redirect->expects($this->once())
->method('back')
->willReturn($this->response);
/** @var Shift $shift */
$shift = Shift::factory(3)->create(['transaction_id' => Uuid::uuid()])->last();
ShiftEntry::factory(2)->create(['shift_id' => $shift->id]);
/** @var EventDispatcher|MockObject $event */
$event = $this->createMock(EventDispatcher::class);
$this->app->instance('events.dispatcher', $event);
$this->setExpects($event, 'dispatch', ['shift.entry.deleting'], [], $this->exactly(2));
/** @var ShiftsController $controller */
$controller = $this->app->make(ShiftsController::class);
$controller->deleteTransaction(new Request([], ['transaction_id' => $shift->transaction_id]));
$this->assertCount(6, Shift::all());
$this->assertCount(3, ShiftEntry::all());
$this->log->hasInfoThatContains('Deleted shift');
$this->log->hasInfoThatContains('shifts with transaction ID');
$this->assertHasNotification('shifts.history.delete.success');
}
public function setUp(): void
{
parent::setUp();
$this->redirect = $this->createMock(Redirector::class);
$this->app->instance(Redirector::class, $this->redirect);
Shift::factory(1)->create(['transaction_id' => null]);
Shift::factory(4)->create(['transaction_id' => Uuid::uuid()]);
$shift = Shift::factory(1)->create(['transaction_id' => Uuid::uuid()])->first();
ShiftEntry::factory(3)->create(['shift_id' => $shift->id]);
}
}