Passed
Push — feature/job-status-transitions ( fcd539 )
by Tristan
05:41
created

JobPoster::job_poster_status_histories()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * Created by Reliese Model.
5
 * Date: Thu, 12 Jul 2018 22:39:27 +0000.
6
 */
7
8
namespace App\Models;
9
10
use Spatie\Translatable\HasTranslations;
11
use Backpack\CRUD\app\Models\Traits\CrudTrait;
12
use Jenssegers\Date\Date;
13
use Illuminate\Notifications\Notifiable;
14
use Illuminate\Support\Facades\App;
15
use Illuminate\Support\Facades\Lang;
16
17
use App\Events\JobSaved;
18
19
/**
20
 * Class JobPoster
21
 *
22
 * @property int $id
23
 * @property int $job_term_id
24
 * @property string $chosen_lang
25
 * @property int $term_qty
26
 * @property \Jenssegers\Date\Date $open_date_time
27
 * @property \Jenssegers\Date\Date $close_date_time
28
 * @property \Jenssegers\Date\Date $start_date_time
29
 * @property \Jenssegers\Date\Date $review_requested_at
30
 * @property \Jenssegers\Date\Date $published_at
31
 * @property int $department_id
32
 * @property int $province_id
33
 * @property int $salary_min
34
 * @property int $salary_max
35
 * @property int $noc
36
 * @property int $classification_id
37
 * @property int $classification_level
38
 * @property int $security_clearance_id
39
 * @property int $language_requirement_id
40
 * @property boolean $remote_work_allowed
41
 * @property int $manager_id
42
 * @property boolean $published
43
 * @property int $team_size
44
 * @property array $work_env_features This should be an array of boolean flags for features, ie json of shape {[feature: string]: boolean}
45
 * @property int $fast_vs_steady
46
 * @property int $horizontal_vs_vertical
47
 * @property int $experimental_vs_ongoing
48
 * @property int $citizen_facing_vs_back_office
49
 * @property int $collaborative_vs_independent
50
 * @property int $telework_allowed_frequency_id
51
 * @property int $flexible_hours_frequency_id
52
 * @property int $travel_requirement_id
53
 * @property int $overtime_requirement_id
54
 * @property int $process_number
55
 * @property int $priority_clearance_number
56
 * @property int $job_poster_status_id
57
 * @property \Jenssegers\Date\Date $loo_issuance_date
58
 * @property \Jenssegers\Date\Date $created_at
59
 * @property \Jenssegers\Date\Date $updated_at
60
 *
61
 * @property int $submitted_applications_count
62
 *
63
 * @property \App\Models\Lookup\Department $department
64
 * @property \App\Models\Lookup\JobTerm $job_term
65
 * @property \App\Models\Lookup\LanguageRequirement $language_requirement
66
 * @property \App\Models\Manager $manager
67
 * @property \App\Models\Lookup\Province $province
68
 * @property \App\Models\Lookup\SecurityClearance $security_clearance
69
 * @property \Illuminate\Database\Eloquent\Collection $criteria
70
 * @property \Illuminate\Database\Eloquent\Collection $job_applications
71
 * @property \Illuminate\Database\Eloquent\Collection $job_poster_key_tasks
72
 * @property \Illuminate\Database\Eloquent\Collection $job_poster_questions
73
 * @property \Illuminate\Database\Eloquent\Collection $job_poster_translations
74
 * @property \Illuminate\Database\Eloquent\Collection $submitted_applications
75
 * @property \Illuminate\Database\Eloquent\Collection $hr_advisors
76
 * @property \App\Models\Lookup\Frequency $telework_allowed_frequency
77
 * @property \App\Models\Lookup\Frequency $flexible_hours_frequency
78
 * @property \App\Models\Lookup\JobPosterStatus $job_poster_status
79
 *
80
 * Localized Properties:
81
 * @property string $city
82
 * @property string $title
83
 * @property string $dept_impact
84
 * @property string $team_impact
85
 * @property string $hire_impact
86
 * @property string $division
87
 * @property string $education
88
 * @property string $work_env_description
89
 * @property string $culture_summary
90
 * @property string $culture_special
91
 *
92
 * Methods
93
 * @method boolean isOpen()
94
 * @method string timeRemaining()
95
 * @method mixed[] toApiArray()
96
 * @method boolean isVisibleToHr()
97
 *
98
 * Computed Properties
99
 * @property string|null $classification_code
100
 * @property string|null $classification_message
101
 * @property int $job_status_id
102
 */
103
class JobPoster extends BaseModel
104
{
105
    use CrudTrait;
106
    use HasTranslations;
107
    use Notifiable;
108
109
    const DATE_FORMAT = [
110
        'en' => 'M jS, Y',
111
        'fr' => 'd M Y',
112
    ];
113
    const TIME_FORMAT = [
114
        'en' => 'h:i A T',
115
        'fr' => 'H \h i T',
116
    ];
117
    const TIMEZONE = 'America/Toronto';
118
119
    /**
120
     * @var string[] $translatable
121
     */
122
    public $translatable = [
123
        'city',
124
        'title',
125
        'dept_impact',
126
        'team_impact',
127
        'hire_impact',
128
        'division',
129
        'education',
130
        'work_env_description',
131
        'culture_summary',
132
        'culture_special',
133
    ];
134
135
    /**
136
     * @var string[] $casts
137
     */
138
    protected $casts = [
139
        'job_term_id' => 'int',
140
        'department_id' => 'int',
141
        'province_id' => 'int',
142
        'salary_min' => 'int',
143
        'salary_max' => 'int',
144
        'noc' => 'int',
145
        'classification_id' => 'int',
146
        'classification_level' => 'int',
147
        'security_clearance_id' => 'int',
148
        'language_requirement_id' => 'int',
149
        'remote_work_allowed' => 'boolean',
150
        'manager_id' => 'int',
151
        'published' => 'boolean',
152
        'team_size' => 'int',
153
        'work_env_features' => 'array',
154
        'fast_vs_steady' => 'int',
155
        'horizontal_vs_vertical' => 'int',
156
        'experimental_vs_ongoing' => 'int',
157
        'citizen_facing_vs_back_office' => 'int',
158
        'collaborative_vs_independent' => 'int',
159
        'telework_allowed_frequency_id' => 'int',
160
        'flexible_hours_frequency_id' => 'int',
161
        'travel_requirement_id' => 'int',
162
        'overtime_requirement_id' => 'int',
163
    ];
164
165
    /**
166
     * @var string[] $dates
167
     */
168
    protected $dates = [
169
        'open_date_time',
170
        'close_date_time',
171
        'start_date_time',
172
        'review_requested_at',
173
        'published_at',
174
        'loo_issuance_date',
175
    ];
176
177
    /**
178
     * @var string[] $fillable
179
     */
180
    protected $fillable = [
181
        'job_term_id',
182
        'chosen_lang',
183
        'term_qty',
184
        'open_date_time',
185
        'close_date_time',
186
        'start_date_time',
187
        'department_id',
188
        'province_id',
189
        'salary_min',
190
        'salary_max',
191
        'noc',
192
        'security_clearance_id',
193
        'language_requirement_id',
194
        'remote_work_allowed',
195
        'published',
196
        'team_size',
197
        'work_env_features',
198
        'fast_vs_steady',
199
        'horizontal_vs_vertical',
200
        'experimental_vs_ongoing',
201
        'citizen_facing_vs_back_office',
202
        'collaborative_vs_independent',
203
        'telework_allowed_frequency_id',
204
        'flexible_hours_frequency_id',
205
        'travel_requirement_id',
206
        'overtime_requirement_id',
207
        'process_number',
208
        'priority_clearance_number',
209
        'loo_issuance_date',
210
        'classification_id',
211
        'classification_level',
212
        'city',
213
        'title',
214
        'dept_impact',
215
        'team_impact',
216
        'hire_impact',
217
        'division',
218
        'education',
219
        'work_env_description',
220
        'culture_summary',
221
        'culture_special',
222
    ];
223
224
    /**
225
     * The attributes that should be visible in arrays.
226
     * In this case, it blocks loaded relations from appearing.
227
     *
228
     * @var array
229
     */
230
    protected $visible = [
231
        'id',
232
        'manager_id',
233
        'chosen_lang',
234
        'term_qty',
235
        'open_date_time',
236
        'close_date_time',
237
        'start_date_time',
238
        'department_id',
239
        'province_id',
240
        'salary_min',
241
        'salary_max',
242
        'noc',
243
        'security_clearance_id',
244
        'language_requirement_id',
245
        'remote_work_allowed',
246
        'published_at',
247
        'published',
248
        'review_requested_at',
249
        'team_size',
250
        'work_env_features',
251
        'fast_vs_steady',
252
        'horizontal_vs_vertical',
253
        'experimental_vs_ongoing',
254
        'citizen_facing_vs_back_office',
255
        'collaborative_vs_independent',
256
        'telework_allowed_frequency_id',
257
        'flexible_hours_frequency_id',
258
        'travel_requirement_id',
259
        'overtime_requirement_id',
260
        'process_number',
261
        'priority_clearance_number',
262
        'loo_issuance_date',
263
        'classification_id',
264
        'classification_level',
265
        'job_status_id',
266
        'city',
267
        'title',
268
        'dept_impact',
269
        'team_impact',
270
        'hire_impact',
271
        'division',
272
        'education',
273
        'work_env_description',
274
        'culture_summary',
275
        'culture_special',
276
    ];
277
278
    /**
279
     * The accessors to append to the model's array form.
280
     *
281
     * @var string[] $appends
282
     */
283
    protected $appends = [
284
        'classification_code',
285
        'classification_message',
286
        'job_status_id',
287
    ];
288
289
    /**
290
     * Eager loaded relationships by default.
291
     *
292
     * @var string[] $with
293
     */
294
    protected $with = [
295
        'criteria',
296
        'manager'
297
    ];
298
299
    /**
300
     * @var mixed[] $dispatchesEvents
301
     */
302
    protected $dispatchesEvents = [
303
        'saved' => JobSaved::class,
304
    ];
305
306
    // @codeCoverageIgnoreStart
307
    public function department() // phpcs:ignore
308
    {
309
        return $this->belongsTo(\App\Models\Lookup\Department::class);
310
    }
311
312
    public function job_term() // phpcs:ignore
313
    {
314
        return $this->belongsTo(\App\Models\Lookup\JobTerm::class);
315
    }
316
317
    public function language_requirement() // phpcs:ignore
318
    {
319
        return $this->belongsTo(\App\Models\Lookup\LanguageRequirement::class);
320
    }
321
322
    public function manager() // phpcs:ignore
323
    {
324
        return $this->belongsTo(\App\Models\Manager::class);
325
    }
326
327
    public function province() // phpcs:ignore
328
    {
329
        return $this->belongsTo(\App\Models\Lookup\Province::class);
330
    }
331
332
    public function security_clearance() // phpcs:ignore
333
    {
334
        return $this->belongsTo(\App\Models\Lookup\SecurityClearance::class);
335
    }
336
337
    public function criteria() // phpcs:ignore
338
    {
339
        return $this->hasMany(\App\Models\Criteria::class);
340
    }
341
342
    public function hr_advisors() // phpcs:ignore
343
    {
344
        return $this->belongsToMany(
345
            \App\Models\HrAdvisor::class,
346
            'claimed_jobs'
347
        );
348
    }
349
350
    public function job_applications() // phpcs:ignore
351
    {
352
        return $this->hasMany(\App\Models\JobApplication::class);
353
    }
354
355
    public function job_poster_key_tasks() // phpcs:ignore
356
    {
357
        return $this->hasMany(\App\Models\JobPosterKeyTask::class);
358
    }
359
360
    public function job_poster_questions() // phpcs:ignore
361
    {
362
        return $this->hasMany(\App\Models\JobPosterQuestion::class);
363
    }
364
365
    public function telework_allowed_frequency() // phpcs:ignore
366
    {
367
        return $this->belongsTo(\App\Models\Lookup\Frequency::class);
368
    }
369
370
    public function flexible_hours_frequency() // phpcs:ignore
371
    {
372
        return $this->belongsTo(\App\Models\Lookup\Frequency::class);
373
    }
374
375
    public function travel_requirement() // phpcs:ignore
376
    {
377
        return $this->belongsTo(\App\Models\Lookup\TravelRequirement::class);
378
    }
379
380
    public function overtime_requirement() // phpcs:ignore
381
    {
382
        return $this->belongsTo(\App\Models\Lookup\OvertimeRequirement::class);
383
    }
384
385
    public function classification() // phpcs:ignore
386
    {
387
        return $this->belongsTo(\App\Models\Classification::class);
388
    }
389
390
    public function comments() // phpcs:ignore
391
    {
392
        return $this->hasMany(\App\Models\Comment::class);
393
    }
394
395
    public function job_poster_status() // phpcs:ignore
396
    {
397
        return $this->belongsTo(\App\Models\Lookup\JobPosterStatus::class);
398
    }
399
400
    public function job_poster_status_histories() // phpcs:ignore
401
    {
402
        return $this->hasMany(\App\Models\JobPosterStatusHistory::class);
403
    }
404
405
    // @codeCoverageIgnoreEnd
406
    /* Artificial Relations */
407
408
    /**
409
     * Get all of the Job Applications submitted to this
410
     * Job Poster.
411
     *
412
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
413
     */
414
    public function submitted_applications() // phpcs:ignore
415
    {
416
        return $this->hasMany(\App\Models\JobApplication::class)->whereDoesntHave('application_status', function ($query): void {
417
            $query->where('name', 'draft');
418
        });
419
    }
420
421
    /* Overrides */
422
423
    /**
424
     * Retrieve the model for a bound value.
425
     * Seems to be a useful workaround for providing submitted_applications_count
426
     * to any bound routes that receive a jobPoster instance without using the
427
     * withCount property on the model itself.
428
     * See https://github.com/laravel/framework/issues/23957 for more info.
429
     *
430
     * @param mixed $value Value used to retrieve the model instance.
431
     *
432
     * @return \Illuminate\Database\Eloquent\Model|null
433
     */
434
    public function resolveRouteBinding($value) // phpcs:ignore
435
    {
436
        return $this->withCount('submitted_applications')->where('id', $value)->first() ?? abort(404);
0 ignored issues
show
Bug introduced by
The function abort was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

436
        return $this->withCount('submitted_applications')->where('id', $value)->first() ?? /** @scrutinizer ignore-call */ abort(404);
Loading history...
437
    }
438
439
    /**
440
     * Intercept setting the "published" attribute, and set the
441
     * "published_at" timestamp if true.
442
     *
443
     * @param mixed $value Incoming value for the 'published' attribute.
444
     *
445
     * @return void
446
     */
447
    public function setPublishedAttribute($value): void
448
    {
449
        if ($value) {
450
            $this->attributes['published_at'] = new Date();
0 ignored issues
show
Bug Best Practice introduced by
The property attributes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
451
        } else {
452
            $this->attributes['published_at'] = null;
453
        }
454
        if ($value === null) {
455
            $value = false;
456
        }
457
        $this->attributes['published'] = $value;
458
    }
459
460
    /* Methods */
461
462
    public function submitted_applications_count() //phpcs:ignore
463
    {
464
        return $this->submitted_applications()->count();
465
    }
466
467
    /**
468
     * Formatted and localized date and time the Job Poster closes.
469
     *
470
     * @return string[]
471
     */
472
    public function applyBy(): array
473
    {
474
        $localCloseDate = new Date($this->close_date_time); // This initializes the date object in UTC time.
475
        $localCloseDate->setTimezone(new \DateTimeZone(self::TIMEZONE)); // Then set the time zone for display.
476
        $displayDate = [
477
            'date' => $localCloseDate->format(self::DATE_FORMAT[App::getLocale()]),
478
            'time' => $localCloseDate->format(self::TIME_FORMAT[App::getLocale()])
479
        ];
480
481
        if (App::isLocale('fr')) {
482
            $displayDate['time'] = str_replace(['EST', 'EDT'], ['HNE', 'HAE'], $displayDate['time']);
483
        }
484
485
        return $displayDate;
486
    }
487
488
    /**
489
     * Return whether the Job is Open or Closed.
490
     * Used by the Admin Portal JobPosterCrudController.
491
     *
492
     * @return string
493
     */
494
    public function displayStatus(): string
495
    {
496
        return $this->isOpen() ? 'Open' : 'Closed';
497
    }
498
499
    /**
500
     * Check if a Job Poster is open for applications.
501
     *
502
     * @return boolean
503
     */
504
    public function isOpen(): bool
505
    {
506
        return $this->published
507
            && $this->open_date_time !== null
508
            && $this->close_date_time !== null
509
            && $this->open_date_time->isPast()
510
            && $this->close_date_time->isFuture();
511
    }
512
513
    /**
514
     * Check if a Job Poster is closed for applications.
515
     *
516
     * @return boolean
517
     */
518
    public function isClosed(): bool
519
    {
520
        return $this->published
521
            && $this->open_date_time !== null
522
            && $this->close_date_time !== null
523
            && $this->open_date_time->isPast()
524
            && $this->close_date_time->isPast();
525
    }
526
527
    /**
528
     * Calculate the remaining time a Job Poster is open.
529
     *
530
     * @return string
531
     */
532
    public function timeRemaining(): string
533
    {
534
        $interval = $this->close_date_time->diff(Date::now());
535
536
        $d = $interval->d;
537
        $h = $interval->h;
538
        $m = $interval->i;
539
        $s = $interval->s;
540
541
        if ($d > 0) {
542
            $unit = 'day';
543
            $count = $d;
544
        } elseif ($h > 0) {
545
            $unit = 'hour';
546
            $count = $h;
547
        } elseif ($m > 0) {
548
            $unit = 'minute';
549
            $count = $m;
550
        } else {
551
            $unit = 'second';
552
            $count = $s;
553
        }
554
555
        $key = "common/time.$unit";
556
557
        return Lang::choice($key, $count);
558
    }
559
560
    /**
561
     * Return the current status for the Job Poster.
562
     * Possible values are "draft", "submitted", "published" and "closed".
563
     *
564
     * @return string
565
     */
566
    public function status(): string
567
    {
568
        $status = 'draft';
569
        if ($this->isOpen()) {
570
            $status = 'published';
571
        } elseif ($this->isClosed()) {
572
            $status = 'closed';
573
        } elseif ($this->review_requested_at !== null) {
574
            $status = 'submitted';
575
        } else {
576
            $status = 'draft';
577
        }
578
579
        return $status;
580
    }
581
582
    /**
583
     * FIXME:
584
     * Return a calculated job status id.
585
     * For now these ids represent the following:
586
     *    1 = Draft
587
     *    2 = Review requested
588
     *    3 = Approved
589
     *    4 = Open
590
     *    5 = Closed
591
     * These statuses needs an immenent refactoring, so I'm not going to create
592
     * a lookup table for them yet.
593
     * TODO: When this is rebuilt, make sure to change matching JobStatus code in
594
     *    resources\assets\js\models\lookupConstants.ts
595
     *
596
     * @return integer
597
     */
598
    public function getJobStatusIdAttribute()
599
    {
600
        $now = new Date();
601
        if ($this->review_requested_at === null) {
602
            return 1; // Draft.
603
        } elseif ($this->published_at === null) {
604
            return 2; // Review requested, but not approved.
605
        } elseif ($this->open_date_time === null || $this->open_date_time >$now) {
606
            return 3; // Approved, but not open.
607
        } elseif ($this->close_date_time === null || $this->close_date_time > $now) {
608
            // Approved and currently open.
609
            return 4; // Open.
610
        } else {
611
            // Published and close date has passed.
612
            return 5; // Closed.
613
        }
614
    }
615
616
    /**
617
     * Return true if this job should be visible to hr advisors.
618
     * It should become visible after Manager has requested a review.
619
     *
620
     * @return boolean
621
     */
622
    public function isVisibleToHr()
623
    {
624
        return $this->job_status_id !== 1;
625
    }
626
627
    /**
628
     * The database model stores a foreign id to the classification table,
629
     * but to simplify the API, this model simply returns the key as classification_code.
630
     *
631
     * @return string|null
632
     */
633
    public function getClassificationCodeAttribute()
634
    {
635
        if ($this->classification_id !== null) {
636
            return $this->classification->key;
637
        }
638
        return null;
639
    }
640
641
    /**
642
     *
643
     * Get the full government classification message.
644
     *
645
     * @return string|null
646
     */
647
    public function getClassificationMessageAttribute()
648
    {
649
        if ($this->classification_id !== null && $this->classification_level !== null) {
650
            return $this->classification->key . '-0' . $this->classification_level;
651
        }
652
        return null;
653
    }
654
}
655