Passed
Push — master ( 49a2c9...58d635 )
by Thomas
07:31
created

Period::studentCount()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 38
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 31
nc 3
nop 1
dl 0
loc 38
rs 9.424
c 0
b 0
f 0
1
<?php
2
3
namespace App\Models;
4
5
use Backpack\CRUD\app\Models\Traits\CrudTrait;
6
use Carbon\Carbon;
7
use Illuminate\Database\Eloquent\Builder;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Support\Facades\DB;
10
use Spatie\Activitylog\Traits\LogsActivity;
11
12
class Period extends Model
13
{
14
    use CrudTrait;
0 ignored issues
show
introduced by
The trait Backpack\CRUD\app\Models\Traits\CrudTrait requires some properties which are not provided by App\Models\Period: $fakeColumns, $identifiableAttribute, $Type
Loading history...
15
    use LogsActivity;
16
17
    public $timestamps = false;
18
19
    protected $fillable = ['name', 'year_id', 'start', 'end'];
20
21
    protected static bool $logUnguarded = true;
22
23
    protected static function boot()
24
    {
25
        parent::boot();
26
27
        static::addGlobalScope('order', function (Builder $builder) {
28
            $builder->orderBy('year_id')->orderBy('order')->orderBy('id');
29
        });
30
    }
31
32
    /**
33
     * Return the current period to be used as a default system-wide.
34
     * First look in Config DB table; otherwise select current or closest next period.
35
     */
36
    public static function get_default_period()
37
    {
38
        $configPeriod = Config::where('name', 'current_period');
39
40
        if ($configPeriod->exists()) {
41
            $currentPeriodId = $configPeriod->first()->value;
42
43
            if (self::where('id', $currentPeriodId)->count() > 0) {
44
                return self::find($currentPeriodId);
45
            } else {
46
                return self::where('end', '>=', date('Y-m-d'))->first();
47
            }
48
        }
49
50
        return self::first();
51
    }
52
53
    /**
54
     * Return the period to preselect for all enrollment-related methods.
55
     */
56
    public static function get_enrollments_period()
57
    {
58
        $selected_period = Config::where('name', 'default_enrollment_period')->first()->value;
59
60
        if (self::where('id', $selected_period)->count() > 0) {
61
            return self::find($selected_period);
62
        } else {
63
            // if the current period ends within 15 days, switch to the next one
64
            $default_period = self::get_default_period();
65
66
            // the number of days between the end and today is 2x less than the number of days between start and end
67
            if (Carbon::parse($default_period->end)->diffInDays() < 0.5 * Carbon::parse($default_period->start)->diffInDays($default_period->end)) {
68
                return self::where('id', '>', $default_period->id)->orderBy('id')->first();
69
            } else {
70
                return $default_period;
71
            }
72
        }
73
    }
74
75
    public function enrollments()
76
    {
77
        return $this->hasManyThrough(Enrollment::class, Course::class)
78
            ->with('course');
79
    }
80
81
    public function courses()
82
    {
83
        return $this->hasMany(Course::class);
84
    }
85
86
    public function internal_courses()
87
    {
88
        return $this->hasMany(Course::class)->internal();
89
    }
90
91
    public function external_courses()
92
    {
93
        return $this->hasMany(Course::class)->external();
94
    }
95
96
    public function year()
97
    {
98
        return $this->belongsTo(Year::class);
99
    }
100
101
    /** returns only pending or paid enrollments, without the child enrollments */
102
    public function real_enrollments()
103
    {
104
        return $this->hasManyThrough(Enrollment::class, Course::class)
105
        ->whereIn('status_id', ['1', '2']) // pending or paid
106
        ->where('parent_id', null);
107
    }
108
109
    /**
110
     * getPendingEnrollmentsCountAttribute
111
     * Do not count children enrollments.
112
     */
113
    public function getPendingEnrollmentsCountAttribute()
114
    {
115
        return $this
116
            ->enrollments
117
            ->where('status_id', 1) // pending
118
            ->where('parent_id', null)
119
            ->count();
120
    }
121
122
    /**
123
     * getPaidEnrollmentsCountAttribute
124
     * Do not count enrollments in children courses.
125
     */
126
    public function getPaidEnrollmentsCountAttribute()
127
    {
128
        return $this
129
            ->enrollments
130
            ->where('status_id', 2) // paid
131
            ->where('parent_id', null)
132
            ->count();
133
    }
134
135
    public function studentCount($gender = null)
136
    {
137
        if (in_array($gender, [1,2])) {
138
            return DB::table('enrollments')
139
                ->join('courses', 'enrollments.course_id', 'courses.id')
140
                ->join('students', 'enrollments.student_id', 'students.id')
141
                ->where('courses.period_id', $this->id)
142
                ->where('enrollments.deleted_at', null)
143
                ->where('enrollments.parent_id', null)
144
                ->where('students.gender_id', $gender)
145
                ->whereIn('enrollments.status_id', ['1', '2']) // filter out cancelled enrollments, todo make this configurable.
146
                ->distinct('student_id')
147
                ->count('enrollments.student_id');
148
        }
149
150
        if ($gender === 0) {
151
            return DB::table('enrollments')
152
                ->join('courses', 'enrollments.course_id', 'courses.id')
153
                ->join('students', 'enrollments.student_id', 'students.id')
154
                ->where('courses.period_id', $this->id)
155
                ->where('enrollments.deleted_at', null)
156
                ->where('enrollments.parent_id', null)
157
                ->where(function($query) {
158
                    return $query->where('students.gender_id', 0)->orWhereNull('students.gender_id');
159
                })
160
                ->whereIn('enrollments.status_id', ['1', '2']) // filter out cancelled enrollments, todo make this configurable.
161
                ->distinct('student_id')
162
                ->count('enrollments.student_id');
163
        }
164
165
        return DB::table('enrollments')
166
            ->join('courses', 'enrollments.course_id', 'courses.id')
167
            ->where('courses.period_id', $this->id)
168
            ->where('enrollments.deleted_at', null)
169
            ->where('enrollments.parent_id', null)
170
            ->whereIn('enrollments.status_id', ['1', '2']) // filter out cancelled enrollments, todo make this configurable.
171
            ->distinct('student_id')
172
            ->count('enrollments.student_id');
173
    }
174
175
    public function getInternalEnrollmentsCountAttribute()
176
    {
177
        return $this->paid_enrollments_count + $this->pending_enrollments_count;
178
    }
179
180
    public function getExternalEnrollmentsCountAttribute()
181
    {
182
        return $this->external_courses->sum('head_count');
183
    }
184
185
    public function getExternalStudentsCountAttribute()
186
    {
187
        return $this->external_courses->sum('new_students');
188
    }
189
190
    public function getExternalCoursesCountAttribute()
191
    {
192
        return $this->external_courses->count();
193
    }
194
195
    public function getPartnershipsCountAttribute()
196
    {
197
        return $this->courses()->pluck('partner_id')->unique()->count();
198
    }
199
200
    public function getPreviousPeriodAttribute()
201
    {
202
        $period = self::where('id', '<', $this->id)->orderBy('id', 'desc')->first();
203
204
        if (! $period == null) {
205
            return $period;
206
        } else {
207
            return self::first();
208
        }
209
    }
210
211
    public function getNextPeriodAttribute()
212
    {
213
        return self::where('id', '>', $this->id)->orderBy('id')->first();
214
    }
215
216
    /** Compute the acquisition rate = the part of students from period P-1 who have been kept in period P */
217
    public function getAcquisitionRateAttribute()
218
    {
219
        // get students enrolled in period P-1
220
        $previous_period_student_ids = $this->previous_period->real_enrollments->pluck('student_id');
221
222
        // and students enrolled in period P
223
        $current_students_ids = $this->real_enrollments->pluck('student_id');
224
225
        // students both in period p-1 and period p
226
        $acquired_students = $previous_period_student_ids->intersect($current_students_ids);
227
228
        return number_format((100 * $acquired_students->count()) / max($previous_period_student_ids->count(), 1), 1).'%';
229
    }
230
231
    public function newStudents()
232
    {
233
        // get students IDs enrolled in all previous periods
234
        $previous_period_student_ids = DB::table('enrollments')->join('courses', 'enrollments.course_id', 'courses.id')->where('period_id', '<', $this->id)->pluck('enrollments.student_id');
235
236
        // and students enrolled in period P
237
        $current_students_ids = $this->real_enrollments->unique('student_id');
238
239
        // students in period P who have never been enrolled in previous periods
240
        return $current_students_ids->whereNotIn('student_id', $previous_period_student_ids);
241
    }
242
243
    public function getPeriodTaughtHoursCountAttribute()
244
    {
245
        // return the sum of all courses' volume for period
246
        return $this->internal_courses->where('parent_course_id', null)->sum('total_volume');
247
    }
248
249
    public function getPeriodSoldHoursCountAttribute()
250
    {
251
        $total = 0;
252
        foreach ($this->courses()->internal()->withCount('real_enrollments')->get() as $course) {
253
            $total += $course->total_volume * $course->real_enrollments_count;
254
        }
255
256
        return $total;
257
    }
258
259
    public function getTakingsAttribute()
260
    {
261
        return $this->real_enrollments->sum('total_paid_price');
262
    }
263
264
    public function getExternalTaughtHoursCountAttribute()
265
    {
266
        // return the sum of all courses' volume for period
267
        return $this->external_courses->where('parent_course_id', null)->sum('total_volume');
268
    }
269
270
    public function getExternalSoldHoursCountAttribute()
271
    {
272
        $total = 0;
273
        foreach ($this->external_courses as $course) {
274
            $total += $course->total_volume * $course->head_count;
275
        }
276
277
        return $total;
278
    }
279
280
    /** TODO this method can be furthered optimized and refactored */
281
    public function getCoursesWithPendingAttendanceAttribute()
282
    {
283
        // get all courses for period and preload relations
284
        $courses = $this->courses()->where(function ($query) {
285
            $query->where('exempt_attendance', '!=', true);
286
            $query->where('exempt_attendance', '!=', 1);
287
            $query->orWhereNull('exempt_attendance');
288
        })->whereHas('events')->with('attendance')->whereNotNull('exempt_attendance')->get();
289
        $coursesWithMissingAttendanceCount = 0;
290
291
        // loop through all courses and get the number of events with incomplete attendance
292
        foreach ($courses as $course) {
293
            foreach ($course->eventsWithExpectedAttendance as $event) {
294
                foreach ($course->enrollments as $enrollment) {
295
                    // if a student has no attendance record for the class (event)
296
                    $hasNotAttended = $course->attendance->where('student_id', $enrollment->student_id)
297
                     ->where('event_id', $event->id)
298
                     ->isEmpty();
299
300
                    // count one and break loop
301
                    if ($hasNotAttended) {
302
                        $coursesWithMissingAttendanceCount++;
303
                        break 2;
304
                    }
305
                }
306
            }
307
        }
308
309
        // sort by number of events with missing attendance
310
        return $coursesWithMissingAttendanceCount;
311
    }
312
}
313