'integer', 'school_id' => 'integer', 'class_section_id' => 'integer', 'updated_by' => 'integer', 'project_index' => 'integer', 'score' => 'decimal:2', // or 'float' if you prefer 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; /* ============================================================ * Relationships (optional but useful) * ============================================================ */ public function student(): BelongsTo { return $this->belongsTo(Student::class, 'student_id'); } public function classSection(): BelongsTo { return $this->belongsTo(ClassSection::class, 'class_section_id'); } public function school(): BelongsTo { return $this->belongsTo(School::class, 'school_id'); } /* ============================================================ * Query Scopes (reusable filters) * ============================================================ */ public function scopeForStudent(Builder $q, int $studentId): Builder { return $q->where('student_id', $studentId); } public function scopeForTerm(Builder $q, string $semester, string $schoolYear): Builder { return $q->where('semester', $semester) ->where('school_year', $schoolYear); } public function scopeForClassSection(Builder $q, ?int $classSectionId): Builder { return $classSectionId !== null ? $q->where('class_section_id', $classSectionId) : $q; } /* ============================================================ * Business: Average score * ============================================================ */ /** * Calculate average project score for a student + term (+ optional class section). * * Enhancements vs CI version: * - Uses SQL AVG for performance * - Ignores NULL, empty string, and non-numeric (best-effort) values * - Returns float|null */ public static function averageScore( int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null ): ?float { $avg = static::query() ->forStudent($studentId) ->forTerm($semester, $schoolYear) ->forClassSection($classSectionId) // ignore NULL and blank ->whereNotNull('score') ->whereRaw("TRIM(COALESCE(score, '')) <> ''") /** * If score is stored as VARCHAR and may contain junk, * this helps filter numeric-only values in MySQL. * If score is a numeric column, you can remove this line. */ ->when(static::scoreMightBeString(), function (Builder $q) { $q->whereRaw("TRIM(score) REGEXP '^-?[0-9]+(\\.[0-9]+)?$'"); }) ->avg('score'); return $avg === null ? null : (float) $avg; } /** * Instance method (so you can keep a similar calling style to CI). */ public function getAverageProjectScore( int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null ): ?float { return static::averageScore($studentId, $semester, $schoolYear, $classSectionId); } /** * Toggle the REGEXP filter depending on your schema. * If your `score` column is DECIMAL/INT/FLOAT, return false. */ protected static function scoreMightBeString(): bool { // Safe default if you're unsure. Set to false if score is numeric. return true; } }