Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
14 / 14
CRAP
100.00% covered (success)
100.00%
1 / 1
Shift
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
14 / 14
26
100.00% covered (success)
100.00%
1 / 1
 neededAngelTypes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 schedule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 scheduleShift
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shiftEntries
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shiftType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 location
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createdBy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updatedBy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 scopeNeedsUsers
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
2
 nextShift
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 previousShift
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 isNightShift
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
9
 getNightShiftMultiplier
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Engelsystem\Models\Shifts;
6
7use Carbon\Carbon;
8use Engelsystem\Models\BaseModel;
9use Engelsystem\Models\Location;
10use Engelsystem\Models\Tag;
11use Engelsystem\Models\User\User;
12use Illuminate\Database\Eloquent\Builder;
13use Illuminate\Database\Eloquent\Collection;
14use Illuminate\Database\Eloquent\Factories\HasFactory;
15use Illuminate\Database\Eloquent\Relations\BelongsTo;
16use Illuminate\Database\Eloquent\Relations\BelongsToMany;
17use Illuminate\Database\Eloquent\Relations\HasMany;
18use Illuminate\Database\Eloquent\Relations\HasOne;
19use Illuminate\Database\Eloquent\Relations\HasOneThrough;
20use Illuminate\Database\Query\Builder as QueryBuilder;
21use Illuminate\Database\Query\Grammars\SQLiteGrammar;
22use Illuminate\Database\Query\JoinClause;
23
24/**
25 * @property int                               $id
26 * @property string                            $title
27 * @property string                            $description
28 * @property string                            $url
29 * @property Carbon                            $start
30 * @property Carbon                            $end
31 * @property int                               $shift_type_id
32 * @property int                               $location_id
33 * @property string|null                       $transaction_id
34 * @property int                               $created_by
35 * @property int|null                          $updated_by
36 * @property Carbon|null                       $created_at
37 * @property Carbon|null                       $updated_at
38 *
39 * @property-read Collection|NeededAngelType[] $neededAngelTypes
40 * @property-read Schedule                     $schedule
41 * @property-read ScheduleShift                $scheduleShift
42 * @property-read Collection|ShiftEntry[]      $shiftEntries
43 * @property-read ShiftType                    $shiftType
44 * @property-read Collection|Tag[]             $tags
45 * @property-read Location                     $location
46 * @property-read User                         $createdBy
47 * @property-read User|null                    $updatedBy
48 *
49 * @method static QueryBuilder|Shift[] whereId($value)
50 * @method static QueryBuilder|Shift[] whereTitle($value)
51 * @method static QueryBuilder|Shift[] whereDescription($value)
52 * @method static QueryBuilder|Shift[] whereUrl($value)
53 * @method static QueryBuilder|Shift[] whereStart($value)
54 * @method static QueryBuilder|Shift[] whereEnd($value)
55 * @method static QueryBuilder|Shift[] whereShiftTypeId($value)
56 * @method static QueryBuilder|Shift[] whereLocationId($value)
57 * @method static QueryBuilder|Shift[] whereTransactionId($value)
58 * @method static QueryBuilder|Shift[] whereCreatedBy($value)
59 * @method static QueryBuilder|Shift[] whereUpdatedBy($value)
60 * @method static QueryBuilder|Shift[] whereCreatedAt($value)
61 * @method static QueryBuilder|Shift[] whereUpdatedAt($value)
62 */
63class Shift extends BaseModel
64{
65    use HasFactory;
66
67    /** @var array<string, string|null> default attributes */
68    protected $attributes = [ // phpcs:ignore
69        'description'    => '',
70        'url'            => '',
71        'transaction_id' => null,
72        'updated_by'     => null,
73    ];
74
75    /** @var bool enable timestamps */
76    public $timestamps = true; // phpcs:ignore
77
78    /** @var array<string, string> */
79    protected $casts = [ // phpcs:ignore
80        'shift_type_id' => 'integer',
81        'location_id'   => 'integer',
82        'created_by'    => 'integer',
83        'updated_by'    => 'integer',
84        'start'         => 'datetime',
85        'end'           => 'datetime',
86    ];
87
88    /** @var array<string> Values that are mass assignable */
89    protected $fillable = [ // phpcs:ignore
90        'title',
91        'description',
92        'url',
93        'start',
94        'end',
95        'shift_type_id',
96        'location_id',
97        'transaction_id',
98        'created_by',
99        'updated_by',
100    ];
101
102    public function neededAngelTypes(): HasMany
103    {
104        return $this->hasMany(NeededAngelType::class);
105    }
106
107    public function schedule(): HasOneThrough
108    {
109        return $this->hasOneThrough(Schedule::class, ScheduleShift::class, null, 'id', null, 'schedule_id');
110    }
111
112    public function scheduleShift(): HasOne
113    {
114        return $this->hasOne(ScheduleShift::class);
115    }
116
117    public function shiftEntries(): HasMany
118    {
119        return $this->hasMany(ShiftEntry::class);
120    }
121
122    public function shiftType(): BelongsTo
123    {
124        return $this->belongsTo(ShiftType::class);
125    }
126
127    public function location(): BelongsTo
128    {
129        return $this->belongsTo(Location::class);
130    }
131
132    public function createdBy(): BelongsTo
133    {
134        return $this->belongsTo(User::class, 'created_by');
135    }
136
137    public function tags(): BelongsToMany
138    {
139        return $this->belongsToMany(Tag::class, 'shift_tags');
140    }
141
142    public function updatedBy(): BelongsTo
143    {
144        return $this->belongsTo(User::class, 'updated_by');
145    }
146
147    public function scopeNeedsUsers(Builder $query): void
148    {
149        $query
150            ->addSelect([
151                // This is "hidden" behind an attribute to not "poison" the SELECT default with fields from added joins
152                'needs_users' => Shift::from('shifts as s2')
153                    ->leftJoin('schedule_shift', 'schedule_shift.shift_id', 's2.id')
154                    ->leftJoin('schedules', 'schedules.id', 'schedule_shift.schedule_id')
155                    ->leftJoin('needed_angel_types', function (JoinClause $join): void {
156                        // Directly
157                        $join->on('needed_angel_types.shift_id', 's2.id')
158                            // Via schedule location
159                            ->orOn('needed_angel_types.location_id', 's2.location_id')
160                            // Via schedule shift type
161                            ->orOn('needed_angel_types.shift_type_id', 'schedules.shift_type');
162                    })
163                    ->whereColumn('s2.id', 'shifts.id')
164                    ->where(function (Builder $query): void {
165                        $query
166                            ->where(function (Builder $query): void {
167                                $query
168                                    // Direct requirement
169                                    ->whereColumn('needed_angel_types.shift_id', 's2.id')
170                                    // Or has schedule & via location
171                                    ->orWhere(function (Builder $query): void {
172                                        $query
173                                            ->where('schedules.needed_from_shift_type', false)
174                                            ->whereColumn('needed_angel_types.location_id', 's2.location_id');
175                                    })
176                                    // Or has schedule & via type
177                                    ->orWhere(function (Builder $query): void {
178                                        $query
179                                            ->where('schedules.needed_from_shift_type', true)
180                                            ->whereColumn('needed_angel_types.shift_type_id', 's2.shift_type_id');
181                                    });
182                            });
183                    })
184                    ->selectRaw('COUNT(*) > 0'),
185            ]);
186
187        if ($query->getConnection()->getQueryGrammar() instanceof SQLiteGrammar) {
188            // SQLite does not support HAVING for non-aggregate queries
189            $query->where('needs_users', '>', 0);
190        } else {
191            // @codeCoverageIgnoreStart
192            // needs_users is defined on select and thus only available after select
193            $query->having('needs_users', '>', 0);
194            // @codeCoverageIgnoreEnd
195        }
196    }
197
198    /**
199     * get next shift with same shift type and location
200     */
201    public function nextShift(): Shift|null
202    {
203        $query = Shift::query();
204        if (Shift::whereTitle($this->title)->where('start', '>', $this->start)->exists()) {
205            $query = $query->where('title', $this->title);
206        }
207        return $query
208            ->where('shift_type_id', $this->shiftType->id)
209            ->where('location_id', $this->location->id)
210            ->where('start', '>', $this->start)
211            ->orderBy('start')
212            ->first();
213    }
214
215    /**
216     * get previous shift with same shift type and location
217     */
218    public function previousShift(): Shift|null
219    {
220        $query = Shift::query();
221        if (Shift::whereTitle($this->title)->where('end', '<', $this->end)->exists()) {
222            $query = $query->where('title', $this->title);
223        }
224        return $query
225            ->where('shift_type_id', $this->shiftType->id)
226            ->where('location_id', $this->location->id)
227            ->where('end', '<', $this->end)
228            ->orderBy('end', 'desc')
229            ->first();
230    }
231
232    /**
233     * Check if the shift is a night shift
234     */
235    public function isNightShift(): bool
236    {
237        $config = config('night_shifts');
238
239        /** @see \Engelsystem\Helpers\Goodie::shiftScoreQuery to keep them in sync */
240        return $config['enabled'] && (
241                // Starts during night
242                $this->start->hour >= $config['start'] && $this->start->hour < $config['end']
243                // Ends during night
244                || (
245                    $this->end->hour > $config['start']
246                    || $this->end->hour == $config['start'] && $this->end->minute > 0
247                ) && $this->end->hour <= $config['end']
248                // Starts before and ends after night
249                || $this->start->hour <= $config['start'] && $this->end->hour >= $config['end']
250            );
251    }
252
253    /**
254     * Calculate the shifts night multiplier
255     */
256    public function getNightShiftMultiplier(): float
257    {
258        if (!$this->isNightShift()) {
259            return 1;
260        }
261
262        return config('night_shifts')['multiplier'];
263    }
264}