Implemented loading angels by shift type on schedule import

This commit is contained in:
Igor Scheller 2023-12-20 18:08:50 +01:00 committed by msquare
parent 909f7bba5a
commit 4bbeb93d64
23 changed files with 467 additions and 62 deletions

View File

@ -17,11 +17,12 @@ class NeededAngelTypeFactory extends Factory
public function definition(): array
{
$forLocation = $this->faker->boolean();
$type = $this->faker->numberBetween(0, 2);
return [
'location_id' => $forLocation ? Location::factory() : null,
'shift_id' => $forLocation ? null : Shift::factory(),
'location_id' => $type == 0 ? Location::factory() : null,
'shift_id' => $type == 1 ? null : Shift::factory(),
'shift_type_id' => $type == 2 ? null : Shift::factory(),
'angel_type_id' => AngelType::factory(),
'count' => $this->faker->numberBetween(1, 5),
];

View File

@ -18,6 +18,7 @@ class ScheduleFactory extends Factory
'name' => $this->faker->unique()->words(4, true),
'url' => $this->faker->parse('https://{{safeEmailDomain}}/{{slug}}.xml'),
'shift_type' => $this->faker->numberBetween(1, 5),
'needed_from_shift_type' => $this->faker->boolean(.2),
'minutes_before' => 15,
'minutes_after' => 15,
];

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Schema\Blueprint;
class ScheduleShiftTypeNeededAngelTypes extends Migration
{
use Reference;
/**
* Run the migration
*/
public function up(): void
{
$this->schema->table('schedules', function (Blueprint $table): void {
$table->boolean('needed_from_shift_type')->after('shift_type')->default(false);
});
$this->schema->table('needed_angel_types', function (Blueprint $table): void {
$this->references($table, 'shift_types')->after('shift_id')->nullable();
});
}
/**
* Reverse the migration
*/
public function down(): void
{
$this->schema->table('schedules', function (Blueprint $table): void {
$table->dropColumn('needed_from_shift_type');
});
$this->schema->table('needed_angel_types', function (Blueprint $table): void {
$table->dropForeign('needed_angel_types_shift_type_id_foreign');
$table->dropColumn('shift_type_id');
});
}
}

View File

@ -74,7 +74,7 @@ function shift_edit_controller()
$angeltypes = AngelType::all()->pluck('name', 'id')->toArray();
$shifttypes = ShiftType::all()->pluck('name', 'id')->toArray();
$needed_angel_types = collect(NeededAngelTypes_by_shift($shift_id))->pluck('count', 'angel_type_id')->toArray();
$needed_angel_types = collect(NeededAngelTypes_by_shift($shift))->pluck('count', 'angel_type_id')->toArray();
foreach (array_keys($angeltypes) as $angeltype_id) {
if (!isset($needed_angel_types[$angeltype_id])) {
$needed_angel_types[$angeltype_id] = 0;

View File

@ -1,47 +1,70 @@
<?php
use Engelsystem\Database\Db;
use Engelsystem\Models\Shifts\Shift;
use Engelsystem\Models\Shifts\ShiftEntry;
use Illuminate\Database\Eloquent\Collection;
/**
* Returns all needed angeltypes and already taken needs.
*
* @param int $shiftId id of shift
* @param Shift $shift
* @return array
*/
function NeededAngelTypes_by_shift($shiftId)
function NeededAngelTypes_by_shift($shift)
{
$needed_angeltypes_source = Db::select(
'
$needed_angeltypes_source = [];
// Select from shift
if (!$shift->schedule) {
$needed_angeltypes_source = Db::select(
'
SELECT
`needed_angel_types`.*,
`angel_types`.`id`,
`angel_types`.`name`,
`angel_types`.`restricted`,
`angel_types`.`shift_self_signup`
FROM `needed_angel_types`
JOIN `angel_types` ON `angel_types`.`id` = `needed_angel_types`.`angel_type_id`
WHERE `shift_id` = ?
ORDER BY `location_id` DESC',
[$shiftId]
);
WHERE `needed_angel_types`.`shift_id` = ?
ORDER BY `location_id` DESC
',
[$shift->id]
);
}
// Use settings from location
if (count($needed_angeltypes_source) == 0) {
// Get needed by shift type
if ($shift->schedule && $shift->schedule->needed_from_shift_type) {
$needed_angeltypes_source = Db::select('
SELECT `needed_angel_types`.*, `angel_types`.`name`, `angel_types`.`restricted`
SELECT
`needed_angel_types`.*,
`angel_types`.`name`,
`angel_types`.`restricted`,
`angel_types`.`shift_self_signup`
FROM `needed_angel_types`
JOIN `angel_types` ON `angel_types`.`id` = `needed_angel_types`.`angel_type_id`
JOIN `shifts` ON `shifts`.`location_id` = `needed_angel_types`.`location_id`
WHERE `shifts`.`id` = ?
WHERE `needed_angel_types`.`shift_type_id` = ?
ORDER BY `location_id` DESC
', [$shiftId]);
', [$shift->shift_type_id]);
}
// Load from room
if ($shift->schedule && !$shift->schedule->needed_from_shift_type) {
$needed_angeltypes_source = Db::select('
SELECT
`needed_angel_types`.*,
`angel_types`.`name`,
`angel_types`.`restricted`,
`angel_types`.`shift_self_signup`
FROM `needed_angel_types`
JOIN `angel_types` ON `angel_types`.`id` = `needed_angel_types`.`angel_type_id`
WHERE `needed_angel_types`.`location_id` = ?
ORDER BY `location_id` DESC
', [$shift->location_id]);
}
/** @var ShiftEntry[]|Collection $shift_entries */
$shift_entries = ShiftEntry::with('user', 'angelType')
->where('shift_id', $shiftId)
->where('shift_id', $shift->id)
->get();
$needed_angeltypes = [];
foreach ($needed_angeltypes_source as $angeltype) {

View File

@ -28,12 +28,26 @@ function Shifts_by_angeltype(AngelType $angeltype)
UNION
/* By shift type */
SELECT DISTINCT `shifts`.* FROM `shifts`
JOIN `needed_angel_types` ON `needed_angel_types`.`shift_type_id` = `shifts`.`shift_type_id`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE `needed_angel_types`.`angel_type_id` = ?
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = TRUE
UNION
/* By location */
SELECT DISTINCT `shifts`.* FROM `shifts`
JOIN `needed_angel_types` ON `needed_angel_types`.`location_id` = `shifts`.`location_id`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE `needed_angel_types`.`angel_type_id` = ?
AND NOT s.shift_id IS NULL
', [$angeltype->id, $angeltype->id]);
AND se.needed_from_shift_type = FALSE
', [$angeltype->id, $angeltype->id, $angeltype->id]);
}
/**
@ -53,7 +67,7 @@ function Shifts_free($start, $end, ShiftsFilter $filter = null)
$shifts = Db::select('
SELECT *
FROM (
SELECT id, start
SELECT shifts.id, start
FROM `shifts`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
WHERE (`end` > ? AND `start` < ?)
@ -64,13 +78,30 @@ function Shifts_free($start, $end, ShiftsFilter $filter = null)
UNION
SELECT id, start
/* By shift type */
SELECT shifts.id, start
FROM `shifts`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE (`end` > ? AND `start` < ?)
AND (SELECT SUM(`count`) FROM `needed_angel_types` WHERE `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id`' . ($filter ? ' AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ')
> (SELECT COUNT(*) FROM `shift_entries` WHERE `shift_entries`.`shift_id`=`shifts`.`id` AND shift_entries.`freeloaded`=0' . ($filter ? ' AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ')
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = TRUE
' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . '
UNION
/* By location */
SELECT shifts.id, start
FROM `shifts`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE (`end` > ? AND `start` < ?)
AND (SELECT SUM(`count`) FROM `needed_angel_types` WHERE `needed_angel_types`.`location_id`=`shifts`.`location_id`' . ($filter ? ' AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ')
> (SELECT COUNT(*) FROM `shift_entries` WHERE `shift_entries`.`shift_id`=`shifts`.`id` AND `freeloaded`=0' . ($filter ? ' AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ')
> (SELECT COUNT(*) FROM `shift_entries` WHERE `shift_entries`.`shift_id`=`shifts`.`id` AND shift_entries.`freeloaded`=0' . ($filter ? ' AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ')
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = FALSE
' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . '
) AS `tmp`
ORDER BY `tmp`.`start`
@ -79,6 +110,8 @@ function Shifts_free($start, $end, ShiftsFilter $filter = null)
$end,
$start,
$end,
$start,
$end,
]);
$shifts = collect($shifts);
@ -110,16 +143,35 @@ function Shifts_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
UNION
/* By shift type */
SELECT DISTINCT `shifts`.*, `shift_types`.`name`, `locations`.`name` AS `location_name`
FROM `shifts`
JOIN `locations` ON `shifts`.`location_id` = `locations`.`id`
JOIN `shift_types` ON `shift_types`.`id` = `shifts`.`shift_type_id`
JOIN `needed_angel_types` ON `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE `shifts`.`location_id` IN (' . implode(',', $shiftsFilter->getLocations()) . ')
AND `start` BETWEEN ? AND ?
AND `needed_angel_types`.`angel_type_id` IN (' . implode(',', $shiftsFilter->getTypes()) . ')
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = TRUE
UNION
/* By location */
SELECT DISTINCT `shifts`.*, `shift_types`.`name`, `locations`.`name` AS `location_name`
FROM `shifts`
JOIN `locations` ON `shifts`.`location_id` = `locations`.`id`
JOIN `shift_types` ON `shift_types`.`id` = `shifts`.`shift_type_id`
JOIN `needed_angel_types` ON `needed_angel_types`.`location_id`=`shifts`.`location_id`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE `shifts`.`location_id` IN (' . implode(',', $shiftsFilter->getLocations()) . ')
AND `start` BETWEEN ? AND ?
AND `needed_angel_types`.`angel_type_id` IN (' . implode(',', $shiftsFilter->getTypes()) . ')
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = FALSE
) AS tmp_shifts
ORDER BY `location_name`, `start`
@ -132,6 +184,8 @@ function Shifts_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
$shiftsFilter->getEnd(),
$shiftsFilter->getStart(),
$shiftsFilter->getEnd(),
$shiftsFilter->getStart(),
$shiftsFilter->getEnd(),
]
);
@ -167,6 +221,27 @@ function NeededAngeltypes_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
UNION
/* By shift type */
SELECT
`needed_angel_types`.*,
`shifts`.`id` AS shift_id,
`angel_types`.`id`,
`angel_types`.`name`,
`angel_types`.`restricted`,
`angel_types`.`shift_self_signup`
FROM `shifts`
JOIN `needed_angel_types` ON `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id`
JOIN `angel_types` ON `angel_types`.`id`= `needed_angel_types`.`angel_type_id`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE `shifts`.`location_id` IN (' . implode(',', $shiftsFilter->getLocations()) . ')
AND shifts.`start` BETWEEN ? AND ?
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = TRUE
UNION
/* By location */
SELECT
`needed_angel_types`.*,
`shifts`.`id` AS shift_id,
@ -178,9 +253,11 @@ function NeededAngeltypes_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
JOIN `needed_angel_types` ON `needed_angel_types`.`location_id`=`shifts`.`location_id`
JOIN `angel_types` ON `angel_types`.`id`= `needed_angel_types`.`angel_type_id`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE `shifts`.`location_id` IN (' . implode(',', $shiftsFilter->getLocations()) . ')
AND shifts.`start` BETWEEN ? AND ?
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = FALSE
';
return Db::select(
@ -190,6 +267,8 @@ function NeededAngeltypes_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
$shiftsFilter->getEnd(),
$shiftsFilter->getStart(),
$shiftsFilter->getEnd(),
$shiftsFilter->getStart(),
$shiftsFilter->getEnd(),
]
);
}
@ -220,6 +299,27 @@ function NeededAngeltype_by_Shift_and_Angeltype(Shift $shift, AngelType $angelty
UNION
/* By shift type */
SELECT
`needed_angel_types`.*,
`shifts`.`id` AS shift_id,
`angel_types`.`id`,
`angel_types`.`name`,
`angel_types`.`restricted`,
`angel_types`.`shift_self_signup`
FROM `shifts`
JOIN `needed_angel_types` ON `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id`
JOIN `angel_types` ON `angel_types`.`id`= `needed_angel_types`.`angel_type_id`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE `shifts`.`id`=?
AND `angel_types`.`id`=?
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = TRUE
UNION
/* By location */
SELECT
`needed_angel_types`.*,
`shifts`.`id` AS shift_id,
@ -231,15 +331,19 @@ function NeededAngeltype_by_Shift_and_Angeltype(Shift $shift, AngelType $angelty
JOIN `needed_angel_types` ON `needed_angel_types`.`location_id`=`shifts`.`location_id`
JOIN `angel_types` ON `angel_types`.`id`= `needed_angel_types`.`angel_type_id`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE `shifts`.`id`=?
AND `angel_types`.`id`=?
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = FALSE
',
[
$shift->id,
$angeltype->id,
$shift->id,
$angeltype->id,
$shift->id,
$angeltype->id,
]
);
}
@ -559,7 +663,7 @@ function Shift($shift)
}
$neededAngels = [];
$angelTypes = NeededAngelTypes_by_shift($shift->id);
$angelTypes = NeededAngelTypes_by_shift($shift);
foreach ($angelTypes as $type) {
$neededAngels[] = [
'angel_type_id' => $type['angel_type_id'],

View File

@ -47,19 +47,36 @@ function stats_hours_to_work(ShiftsFilter $filter = null)
* TIMESTAMPDIFF(MINUTE, `shifts`.`start`, `shifts`.`end`) / 60 AS `count`
FROM `shifts`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
WHERE `shifts`.`end` >= NOW()
WHERE shifts.`end` >= NOW()
AND s.shift_id IS NULL
' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . '
UNION ALL
/* By shift type */
SELECT
(SELECT SUM(`count`) FROM `needed_angel_types` WHERE `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id`' . ($filter ? ' AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ')
* TIMESTAMPDIFF(MINUTE, `shifts`.`start`, `shifts`.`end`) / 60 AS `count`
FROM `shifts`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE shifts.`end` >= NOW()
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = TRUE
' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . '
UNION ALL
/* By location */
SELECT
(SELECT SUM(`count`) FROM `needed_angel_types` WHERE `needed_angel_types`.`location_id`=`shifts`.`location_id`' . ($filter ? ' AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . ')
* TIMESTAMPDIFF(MINUTE, `shifts`.`start`, `shifts`.`end`) / 60 AS `count`
FROM `shifts`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
WHERE shifts.`end` >= NOW()
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE shifts.`end` >= NOW()
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = FALSE
' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . '
) AS `tmp`
'
@ -90,7 +107,8 @@ function stats_angels_needed_three_hours(ShiftsFilter $filter = null)
AND `needed_angel_types`.`shift_id`=`shifts`.`id`
' . ($filter ? 'AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . '
) - (
SELECT COUNT(*) FROM `shift_entries`
SELECT COUNT(*)
FROM `shift_entries`
JOIN `angel_types` ON `angel_types`.`id`=`shift_entries`.`angel_type_id`
WHERE `angel_types`.`show_on_dashboard`=TRUE
AND `shift_entries`.`shift_id`=`shifts`.`id`
@ -107,6 +125,38 @@ function stats_angels_needed_three_hours(ShiftsFilter $filter = null)
UNION ALL
/* By shift type */
SELECT
GREATEST(0,
(
SELECT SUM(needed_angel_types.`count`)
FROM `needed_angel_types`
JOIN `angel_types` ON `angel_types`.`id`=`needed_angel_types`.`angel_type_id`
WHERE `angel_types`.`show_on_dashboard`=TRUE
AND `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id`
' . ($filter ? 'AND needed_angel_types.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . '
) - (
SELECT COUNT(*)
FROM `shift_entries`
JOIN `angel_types` ON `angel_types`.`id`=`shift_entries`.`angel_type_id`
WHERE `angel_types`.`show_on_dashboard`=TRUE
AND `shift_entries`.`shift_id`=`shifts`.`id`
AND `freeloaded`=0
' . ($filter ? 'AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . '
)
)
AS `count`
FROM `shifts`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE shifts.`end` > NOW() AND shifts.`start` < ?
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = TRUE
' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . '
UNION ALL
/* By location */
SELECT
GREATEST(0,
(
@ -129,12 +179,15 @@ function stats_angels_needed_three_hours(ShiftsFilter $filter = null)
AS `count`
FROM `shifts`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
WHERE `end` > NOW() AND `start` < ?
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE shifts.`end` > NOW() AND shifts.`start` < ?
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = FALSE
' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . '
) AS `tmp`', [
$in3hours,
$in3hours,
$in3hours,
]);
return $result['count'] ?: '-';
@ -189,6 +242,37 @@ function stats_angels_needed_for_nightshifts(ShiftsFilter $filter = null)
UNION ALL
/* By shift type */
SELECT
GREATEST(0,
(
SELECT SUM(needed_angel_types.`count`)
FROM `needed_angel_types`
JOIN `angel_types` ON `angel_types`.`id`=`needed_angel_types`.`angel_type_id`
WHERE `angel_types`.`show_on_dashboard`=TRUE
AND `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id`
' . ($filter ? 'AND angel_types.id IN (' . implode(',', $filter->getTypes()) . ')' : '') . '
) - (
SELECT COUNT(*) FROM `shift_entries`
JOIN `angel_types` ON `angel_types`.`id`=`shift_entries`.`angel_type_id`
WHERE `angel_types`.`show_on_dashboard`=TRUE
AND `shift_entries`.`shift_id`=`shifts`.`id`
AND shift_entries.`freeloaded`=0
' . ($filter ? 'AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . '
)
)
AS `count`
FROM `shifts`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE shifts.`end` > ? AND shifts.`start` < ?
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = TRUE
' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . '
UNION ALL
/* By location */
SELECT
GREATEST(0,
(
@ -203,21 +287,25 @@ function stats_angels_needed_for_nightshifts(ShiftsFilter $filter = null)
JOIN `angel_types` ON `angel_types`.`id`=`shift_entries`.`angel_type_id`
WHERE `angel_types`.`show_on_dashboard`=TRUE
AND `shift_entries`.`shift_id`=`shifts`.`id`
AND `freeloaded`=0
AND shift_entries.`freeloaded`=0
' . ($filter ? 'AND shift_entries.angel_type_id IN (' . implode(',', $filter->getTypes()) . ')' : '') . '
)
)
AS `count`
FROM `shifts`
LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id
WHERE `end` > ? AND `start` < ?
LEFT JOIN schedules AS se on s.schedule_id = se.id
WHERE shifts.`end` > ? AND shifts.`start` < ?
AND NOT s.shift_id IS NULL
AND se.needed_from_shift_type = FALSE
' . ($filter ? 'AND shifts.location_id IN (' . implode(',', $filter->getLocations()) . ')' : '') . '
) AS `tmp`', [
$night_start,
$night_end,
$night_start,
$night_end,
$night_start,
$night_end,
]);
return $result['count'] ?: '-';

View File

@ -171,7 +171,9 @@ function admin_shifts()
}
if ($request->has('angelmode')) {
if ($request->input('angelmode') == 'location') {
if ($request->input('angelmode') == 'shift_type') {
$angelmode = 'shift_type';
} elseif ($request->input('angelmode') == 'location') {
$angelmode = 'location';
} elseif ($request->input('angelmode') == 'manually') {
foreach ($types as $type) {
@ -203,7 +205,11 @@ function admin_shifts()
// Alle Eingaben in Ordnung
if ($valid) {
if ($angelmode == 'location') {
if ($angelmode == 'shift_type') {
$needed_angel_types = NeededAngelType::whereShiftTypeId($shifttype_id)
->pluck('count', 'angel_type_id')
->toArray() + $needed_angel_types;
} elseif ($angelmode == 'location') {
$needed_angel_types = NeededAngelType::whereLocationId($lid)
->pluck('count', 'angel_type_id')
->toArray() + $needed_angel_types;
@ -541,7 +547,13 @@ function admin_shifts()
form_info(__('Needed angels')),
form_radio(
'angelmode',
__('Take needed angels from location settings'),
__('Copy needed angels from shift type settings'),
$angelmode == 'shift_type',
'shift_type'
),
form_radio(
'angelmode',
__('Copy needed angels from location settings'),
$angelmode == 'location',
'location'
),

View File

@ -118,6 +118,7 @@ class ImportSchedule extends BaseController
'name' => 'required',
'url' => 'required',
'shift_type' => 'required|int',
'needed_from_shift_type' => 'optional|checked',
'minutes_before' => 'int',
'minutes_after' => 'int',
]);
@ -129,17 +130,19 @@ class ImportSchedule extends BaseController
$schedule->name = $data['name'];
$schedule->url = $data['url'];
$schedule->shift_type = $data['shift_type'];
$schedule->needed_from_shift_type = (bool) $data['needed_from_shift_type'];
$schedule->minutes_before = $data['minutes_before'];
$schedule->minutes_after = $data['minutes_after'];
$schedule->save();
$this->log->info(
'Schedule {name}: Url {url}, Shift Type {shift_type}, minutes before/after {before}/{after}',
'Schedule {name}: Url {url}, Shift Type {shift_type}, ({need}), minutes before/after {before}/{after}',
[
'name' => $schedule->name,
'url' => $schedule->name,
'shift_type' => $schedule->shift_type,
'need' => $schedule->needed_from_shift_type ? 'from shift type' : 'from room',
'before' => $schedule->minutes_before,
'after' => $schedule->minutes_after,
]
@ -169,8 +172,8 @@ class ImportSchedule extends BaseController
''
);
$this->fireDeleteShiftEntryEvents($event);
$this->deleteEvent($event);
$this->fireDeleteShiftEntryEvents($event, $schedule);
$this->deleteEvent($event, $schedule);
}
$schedule->delete();
@ -271,13 +274,14 @@ class ImportSchedule extends BaseController
$shiftType,
$locations
->where('name', $event->getRoom()->getName())
->first()
->first(),
$scheduleUrl
);
}
foreach ($deleteEvents as $event) {
$this->fireDeleteShiftEntryEvents($event);
$this->deleteEvent($event);
$this->fireDeleteShiftEntryEvents($event, $scheduleUrl);
$this->deleteEvent($event, $scheduleUrl);
}
$scheduleUrl->touch();
@ -296,7 +300,7 @@ class ImportSchedule extends BaseController
$this->log('Created schedule location "{location}"', ['location' => $room->getName()]);
}
protected function fireDeleteShiftEntryEvents(Event $event): void
protected function fireDeleteShiftEntryEvents(Event $event, ScheduleUrl $schedule): void
{
$shiftEntries = $this->db
->table('shift_entries')
@ -310,6 +314,7 @@ class ImportSchedule extends BaseController
->join('angel_types', 'angel_types.id', 'shift_entries.angel_type_id')
->join('shift_types', 'shift_types.id', 'shifts.shift_type_id')
->where('schedule_shift.guid', $event->getGuid())
->where('schedule_shift.schedule_id', $schedule->id)
->get();
foreach ($shiftEntries as $shiftEntry) {
@ -359,13 +364,13 @@ class ImportSchedule extends BaseController
);
}
protected function updateEvent(Event $event, int $shiftTypeId, Location $location): void
protected function updateEvent(Event $event, int $shiftTypeId, Location $location, ScheduleUrl $schedule): void
{
$user = auth()->user();
$eventTimeZone = Carbon::now()->timezone;
/** @var ScheduleShift $scheduleShift */
$scheduleShift = ScheduleShift::whereGuid($event->getGuid())->first();
$scheduleShift = ScheduleShift::whereGuid($event->getGuid())->where('schedule_id', $schedule->id)->first();
$shift = $scheduleShift->shift;
$shift->title = $event->getTitle();
$shift->shift_type_id = $shiftTypeId;
@ -388,10 +393,10 @@ class ImportSchedule extends BaseController
);
}
protected function deleteEvent(Event $event): void
protected function deleteEvent(Event $event, ScheduleUrl $schedule): void
{
/** @var ScheduleShift $scheduleShift */
$scheduleShift = ScheduleShift::whereGuid($event->getGuid())->first();
$scheduleShift = ScheduleShift::whereGuid($event->getGuid())->where('schedule_id', $schedule->id)->first();
$shift = $scheduleShift->shift;
$shift->delete();
@ -509,10 +514,10 @@ class ImportSchedule extends BaseController
$scheduleEventsGuidList = array_keys($scheduleEvents);
$existingShifts = $this->getScheduleShiftsByGuid($scheduleUrl, $scheduleEventsGuidList);
foreach ($existingShifts as $shift) {
$guid = $shift->guid;
foreach ($existingShifts as $scheduleShift) {
$guid = $scheduleShift->guid;
/** @var Shift $shift */
$shift = Shift::with('location')->find($shift->shift_id);
$shift = Shift::with('location')->find($scheduleShift->shift_id);
$event = $scheduleEvents[$guid];
$location = $locations->where('name', $event->getRoom()->getName())->first();
@ -535,8 +540,8 @@ class ImportSchedule extends BaseController
}
$scheduleShifts = $this->getScheduleShiftsWhereNotGuid($scheduleUrl, $scheduleEventsGuidList);
foreach ($scheduleShifts as $shift) {
$event = $this->eventFromScheduleShift($shift);
foreach ($scheduleShifts as $scheduleShift) {
$event = $this->eventFromScheduleShift($scheduleShift);
$deleteEvents[$event->getGuid()] = $event;
}
@ -545,8 +550,7 @@ class ImportSchedule extends BaseController
protected function eventFromScheduleShift(ScheduleShift $scheduleShift): Event
{
/** @var Shift $shift */
$shift = Shift::with('location')->find($scheduleShift->shift_id);
$shift = $scheduleShift->shift;
$duration = $shift->start->diff($shift->end);
return new Event(

View File

@ -126,7 +126,9 @@ function load_locations(bool $onlyWithActiveShifts = false)
$locationIdsFromShift = Shift::query()
->leftJoin('needed_angel_types', 'shifts.id', 'needed_angel_types.shift_id')
->whereNotNull('needed_angel_types.shift_id')
->leftJoin('needed_angel_types AS nast', 'shifts.shift_type_id', 'nast.shift_type_id')
->whereNotNull('needed_angel_types.id')
->orWhereNotNull('nast.id')
->select('shifts.location_id');
$locations->whereIn('id', $locationIdsFromAngelType)

View File

@ -722,8 +722,11 @@ msgstr "Schichtwechsel-Stunden"
msgid "Create a shift over midnight."
msgstr "Erstelle Schichten über Mitternacht"
msgid "Take needed angels from location settings"
msgstr "Übernehme benötigte Engel von den Ort-Einstellungen"
msgid "Copy needed angels from location settings"
msgstr "Kopiere benötigte Engel von den Ort-Einstellungen"
msgid "Copy needed angels from shift type settings"
msgstr "Kopiere benötigte Engel von den Schichttyp-Einstellungen"
msgid "The following angels are needed"
msgstr "Die folgenden Engel werden benötigt"
@ -1447,6 +1450,9 @@ msgstr "Programm URL (schedule.xml)"
msgid "schedule.shift-type"
msgstr "Schichttyp"
msgid "schedule.needed-from-shift-type"
msgstr "Engeltypen vom Schichttyp laden (sonst vom Ort)"
msgid "schedule.minutes-before"
msgstr "Minuten vor Talk beginn hinzufügen"
@ -1934,7 +1940,7 @@ msgid "location.map_url"
msgstr "Karte"
msgid "location.required_angels"
msgstr "Benötigte Engel"
msgstr "Benötigte Engel (bei Fahrplan import)"
msgid "location.map_url.info"
msgstr "Die Karte wird auf der Ort-Seite als iframe eingebettet."
@ -1960,6 +1966,9 @@ msgstr "Schichttyp erstellen"
msgid "shifttype.delete.title"
msgstr "Schichttyp \"%s\" löschen"
msgid "shifttype.required_angels"
msgstr "Benötigte Engel (bei Fahrplan import)"
msgid "event.day"
msgstr "Tag %1$d"

View File

@ -151,6 +151,9 @@ msgstr "Schedule URL (schedule.xml)"
msgid "schedule.shift-type"
msgstr "Shift type"
msgid "schedule.needed-from-shift-type"
msgstr "Load angel types from shift type (else from location)"
msgid "schedule.minutes-before"
msgstr "Add minutes before talk begins"
@ -664,7 +667,7 @@ msgid "location.map_url"
msgstr "Map"
msgid "location.required_angels"
msgstr "Required angels"
msgstr "Required angels (on schedule import)"
msgid "location.map_url.info"
msgstr "The map will be embedded on the location page as an iframe."
@ -690,6 +693,9 @@ msgstr "Create shift type"
msgid "shifttype.delete.title"
msgstr "Delete shift type \"%s\""
msgid "shifttype.required_angels"
msgstr "Required angels (on schedule import)"
msgid "event.day"
msgstr "Day %1$d"

View File

@ -34,6 +34,10 @@
'selected': schedule ? schedule.shift_type : '',
}) }}
{{ f.checkbox('needed_from_shift_type', __('schedule.needed-from-shift-type'), {
'checked': schedule ? schedule.needed_from_shift_type : '',
}) }}
{{ f.input('minutes_before', __('schedule.minutes-before'), {
'type': 'number',
'required': true,

View File

@ -23,6 +23,27 @@
'info': __('form.markdown')
}) }}
</div>
<div class="col-lg-6">
<h4>{{ __('shifttype.required_angels') }}</h4>
{% for types in angel_types.chunk(3) %}
<div class="row">
{% for angel_type in types %}
{% set needed = shifttype
? shifttype.neededAngelTypes.where('angel_type_id', angel_type.id).first()
: null %}
{% set name = 'angel_type_' ~ angel_type.id %}
<div class="col-md-4">
{{ f.number(name, angel_type.name, {
'value': f.formData(name, needed ? needed.count : 0),
'min': 0,
'step': 1,
}) }}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
<div class="col-md-12">

View File

@ -11,7 +11,10 @@ use Engelsystem\Http\Redirector;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\Shifts\NeededAngelType;
use Engelsystem\Models\Shifts\ShiftType;
use Illuminate\Database\Eloquent\Collection;
use Psr\Log\LoggerInterface;
class ShiftTypesController extends BaseController
@ -48,10 +51,15 @@ class ShiftTypesController extends BaseController
$shiftTypeId = (int) $request->getAttribute('shift_type_id');
$shiftType = $this->shiftType->find($shiftTypeId);
$angeltypes = AngelType::all()
->sortBy('name');
return $this->response->withView(
'admin/shifttypes/edit',
['shifttype' => $shiftType]
[
'shifttype' => $shiftType,
'angel_types' => $angeltypes,
]
);
}
@ -77,12 +85,19 @@ class ShiftTypesController extends BaseController
return $this->delete($request);
}
/** @var Collection|AngelType[] $angelTypes */
$angelTypes = AngelType::all();
$validation = [];
foreach ($angelTypes as $angelType) {
$validation['angel_type_' . $angelType->id] = 'optional|int';
}
$data = $this->validate(
$request,
[
'name' => 'required',
'description' => 'required|optional',
]
] + $validation
);
if (ShiftType::whereName($data['name'])->where('id', '!=', $shiftType->id)->exists()) {
@ -93,12 +108,33 @@ class ShiftTypesController extends BaseController
$shiftType->description = $data['description'];
$shiftType->save();
$shiftType->neededAngelTypes()->delete();
$angelsInfo = '';
foreach ($angelTypes as $angelType) {
$count = $data['angel_type_' . $angelType->id];
if (!$count) {
continue;
}
$neededAngelType = new NeededAngelType();
$neededAngelType->shiftType()->associate($shiftType);
$neededAngelType->angelType()->associate($angelType);
$neededAngelType->count = $data['angel_type_' . $angelType->id];
$neededAngelType->save();
$angelsInfo .= sprintf(', %s: %s', $angelType->name, $count);
}
$this->log->info(
'Updated shift type "{name}": {description}',
'Updated shift type "{name}": {description} {angels}',
[
'name' => $shiftType->name,
'description' => $shiftType->description,
'angels' => $angelsInfo,
]
);

View File

@ -78,6 +78,10 @@ class ShiftsController extends BaseController
$query->on('needed_angel_types.shift_id', '=', 'shifts.id')
->whereNull('schedule_shift.shift_id');
})
->leftJoin('needed_angel_types AS nast', function (JoinClause $query): void {
$query->on('nast.shift_type_id', '=', 'shifts.shift_type_id')
->whereNotNull('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');
@ -88,6 +92,7 @@ class ShiftsController extends BaseController
->where(function (EloquentBuilder $query) use ($angelTypes): void {
$query
->whereIn('needed_angel_types.angel_type_id', $angelTypes)
->orWhereIn('nast.angel_type_id', $angelTypes)
->orWhereIn('nas.angel_type_id', $angelTypes);
})
// Starts soon
@ -98,7 +103,7 @@ class ShiftsController extends BaseController
->from('shift_entries')
->selectRaw('COUNT(*)')
->where(fn(Builder $query) => $this->queryShiftEntries($query));
}, '<', Shift::query()->raw('COALESCE(needed_angel_types.count, nas.count)'))
}, '<', Shift::query()->raw('COALESCE(needed_angel_types.count, nast.count, nas.count)'))
->limit(10)
->orderBy('start');

View File

@ -15,16 +15,19 @@ use Illuminate\Database\Query\Builder as QueryBuilder;
* @property int $id
* @property int|null $location_id
* @property int|null $shift_id
* @property int|null $shift_type_id
* @property int $angel_type_id
* @property int $count
*
* @property-read Location|null $location
* @property-read Shift|null $shift
* @property-read ShiftType|null $shiftType
* @property-read AngelType $angelType
*
* @method static QueryBuilder|NeededAngelType[] whereId($value)
* @method static QueryBuilder|NeededAngelType[] whereLocationId($value)
* @method static QueryBuilder|NeededAngelType[] whereShiftId($value)
* @method static QueryBuilder|NeededAngelType[] whereShiftTypeId($value)
* @method static QueryBuilder|NeededAngelType[] whereAngelTypeId($value)
* @method static QueryBuilder|NeededAngelType[] whereCount($value)
*/
@ -36,12 +39,14 @@ class NeededAngelType extends BaseModel
protected $attributes = [ // phpcs:ignore
'location_id' => null,
'shift_id' => null,
'shift_type_id' => null,
];
/** @var array<string> */
protected $fillable = [ // phpcs:ignore
'location_id',
'shift_id',
'shift_type_id',
'angel_type_id',
'count',
];
@ -56,6 +61,11 @@ class NeededAngelType extends BaseModel
return $this->belongsTo(Shift::class);
}
public function shiftType(): BelongsTo
{
return $this->belongsTo(ShiftType::class);
}
public function angelType(): BelongsTo
{
return $this->belongsTo(AngelType::class);

View File

@ -18,6 +18,7 @@ use Illuminate\Database\Query\Builder as QueryBuilder;
* @property string $name
* @property string $url
* @property int $shift_type
* @property bool $needed_from_shift_type
* @property int $minutes_before
* @property int $minutes_after
* @property Carbon $created_at
@ -31,6 +32,7 @@ use Illuminate\Database\Query\Builder as QueryBuilder;
* @method static QueryBuilder|Schedule[] whereName($value)
* @method static QueryBuilder|Schedule[] whereUrl($value)
* @method static QueryBuilder|Schedule[] whereShiftType($value)
* @method static QueryBuilder|Schedule[] whereNeededFromShiftType($value)
* @method static QueryBuilder|Schedule[] whereMinutesBefore($value)
* @method static QueryBuilder|Schedule[] whereMinutesAfter($value)
* @method static QueryBuilder|Schedule[] whereCreatedAt($value)
@ -46,6 +48,7 @@ class Schedule extends BaseModel
/** @var array<string> */
protected $casts = [ // phpcs:ignore
'shift_type' => 'integer',
'needed_from_shift_type' => 'boolean',
'minutes_before' => 'integer',
'minutes_after' => 'integer',
];
@ -55,6 +58,7 @@ class Schedule extends BaseModel
'name',
'url',
'shift_type',
'needed_from_shift_type',
'minutes_before',
'minutes_after',
];

View File

@ -15,6 +15,7 @@ use Illuminate\Database\Query\Builder as QueryBuilder;
* @property string $name
* @property string $description
*
* @property-read Collection|NeededAngelType[] $neededAngelTypes
* @property-read Collection|Schedule[] $schedules
* @property-read Collection|Shift[] $shifts
*
@ -32,6 +33,11 @@ class ShiftType extends BaseModel
'description',
];
public function neededAngelTypes(): HasMany
{
return $this->hasMany(NeededAngelType::class);
}
public function schedules(): HasMany
{
return $this->hasMany(Schedule::class, 'shift_type');

View File

@ -11,6 +11,7 @@ use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Request;
use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\Shifts\ShiftEntry;
use Engelsystem\Models\Shifts\ShiftType;
use Engelsystem\Models\Shifts\Shift;
@ -18,7 +19,7 @@ use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\Controllers\ControllerTest;
use PHPUnit\Framework\MockObject\MockObject;
class ShiftTypeControllerTest extends ControllerTest
class ShiftTypesControllerTest extends ControllerTest
{
protected Redirector|MockObject $redirect;
@ -82,6 +83,7 @@ class ShiftTypeControllerTest extends ControllerTest
$this->assertEquals($shifttype->id, $data['shifttype']?->id);
$this->assertNotEmpty($data['shifttype']?->name);
$this->assertNotEmpty($data['shifttype']?->description);
$this->assertNotNull($data['angel_types']);
return $this->response;
});
@ -104,6 +106,7 @@ class ShiftTypeControllerTest extends ControllerTest
$this->assertEquals('admin/shifttypes/edit', $view);
$this->assertArrayHasKey('shifttype', $data);
$this->assertNull($data['shifttype']);
$this->assertNotNull($data['angel_types']);
return $this->response;
});
@ -115,6 +118,8 @@ class ShiftTypeControllerTest extends ControllerTest
*/
public function testSave(): void
{
$angelType = AngelType::factory(2)->create()->first();
/** @var ShiftTypesController $controller */
$controller = $this->app->make(ShiftTypesController::class);
$controller->setValidator(new Validator());
@ -124,6 +129,8 @@ class ShiftTypeControllerTest extends ControllerTest
$this->request = $this->request->withParsedBody([
'name' => 'Test shift type',
'description' => 'Something',
'angel_type_' . $angelType->id => 3,
'angel_type_' . $angelType->id + 1 => 0,
]);
$controller->save($this->request);
@ -131,6 +138,7 @@ class ShiftTypeControllerTest extends ControllerTest
$this->assertTrue($this->log->hasInfoThatContains('Updated shift type'));
$this->assertHasNotification('shifttype.edit.success');
$this->assertCount(1, ShiftType::whereName('Test shift type')->get());
$this->assertCount(1, ShiftType::first()->neededAngelTypes);
}
/**

View File

@ -8,6 +8,7 @@ use Engelsystem\Models\AngelType;
use Engelsystem\Models\Location;
use Engelsystem\Models\Shifts\NeededAngelType;
use Engelsystem\Models\Shifts\Shift;
use Engelsystem\Models\Shifts\ShiftType;
use Engelsystem\Test\Unit\Models\ModelTest;
class NeededAngelTypeTest extends ModelTest
@ -15,6 +16,7 @@ class NeededAngelTypeTest extends ModelTest
/**
* @covers \Engelsystem\Models\Shifts\NeededAngelType::location
* @covers \Engelsystem\Models\Shifts\NeededAngelType::shift
* @covers \Engelsystem\Models\Shifts\NeededAngelType::shiftType
* @covers \Engelsystem\Models\Shifts\NeededAngelType::angelType
*/
public function testShift(): void
@ -23,12 +25,15 @@ class NeededAngelTypeTest extends ModelTest
$location = Location::factory()->create();
/** @var Shift $shift */
$shift = Shift::factory()->create();
/** @var ShiftType $shiftType */
$shiftType = ShiftType::factory()->create();
/** @var AngelType $angelType */
$angelType = AngelType::factory()->create();
$model = new NeededAngelType();
$model->location()->associate($location);
$model->shift()->associate($shift);
$model->shiftType()->associate($shiftType);
$model->angelType()->associate($angelType);
$model->count = 3;
$model->save();
@ -36,6 +41,7 @@ class NeededAngelTypeTest extends ModelTest
$model = NeededAngelType::find(1);
$this->assertEquals($location->id, $model->location->id);
$this->assertEquals($shift->id, $model->shift->id);
$this->assertEquals($shiftType->id, $model->shiftType->id);
$this->assertEquals($angelType->id, $model->angelType->id);
$this->assertEquals(3, $model->count);
}

View File

@ -17,6 +17,7 @@ class ScheduleTest extends ModelTest
'url' => 'https://foo.bar/schedule.xml',
'name' => 'Testing',
'shift_type' => 1,
'needed_from_shift_type' => false,
'minutes_before' => 10,
'minutes_after' => 10,
];

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Engelsystem\Test\Unit\Models\Shifts;
use Engelsystem\Models\Shifts\NeededAngelType;
use Engelsystem\Models\Shifts\Schedule;
use Engelsystem\Models\Shifts\Shift;
use Engelsystem\Models\Shifts\ShiftType;
@ -11,6 +12,19 @@ use Engelsystem\Test\Unit\Models\ModelTest;
class ShiftTypeTest extends ModelTest
{
/**
* @covers \Engelsystem\Models\Shifts\ShiftType::neededAngelTypes
*/
public function testNeededAngelTypes(): void
{
$shiftType = new ShiftType(['name' => 'Another type', 'description' => '']);
$shiftType->save();
NeededAngelType::factory()->create(['shift_type_id' => 1]);
$this->assertCount(1, ShiftType::find(1)->neededAngelTypes);
}
/**
* @covers \Engelsystem\Models\Shifts\ShiftType::schedules
*/