schoolYears = $schoolYears instanceof SchoolYearContextService ? $schoolYears : app(SchoolYearContextService::class); } public function listReports(User $user, array $filters): LengthAwarePaginator { $filterSectionIds = []; if (! empty($filters['class_section_id'])) { $filterSectionIds = $this->equivalentClassSectionIds((int) $filters['class_section_id']); } $query = ClassProgressReport::query() ->with(['classSection', 'teacher']) ->orderBy($filters['sort_by'] ?? 'week_start', $filters['sort_dir'] ?? 'desc'); if (! $this->isAdmin($user)) { if ($this->isParent($user)) { $parentSectionIds = $this->parentSectionIds((int) $user->id); if (! empty($filters['class_section_id'])) { $sid = (int) $filters['class_section_id']; if (! in_array($sid, $parentSectionIds, true)) { $query->whereRaw('1 = 0'); } } elseif ($parentSectionIds !== []) { $query->whereIn('class_progress_reports.class_section_id', $parentSectionIds); } else { $query->whereRaw('1 = 0'); } } else { $relaxedSectionIds = TeacherClass::distinctAccessibleSectionIdsForTeacher((int) $user->id); if (! empty($filters['class_section_id'])) { $requestedSectionIds = $filterSectionIds !== [] ? $filterSectionIds : [(int) $filters['class_section_id']]; // Section filter: if this user has ever taught the section, show all rows for that section // (legacy DB often has `class_progress_reports` without matching current-year `teacher_class`). if (array_intersect($requestedSectionIds, $relaxedSectionIds) === []) { $query->whereRaw('1 = 0'); } } elseif ($relaxedSectionIds !== []) { $query->whereIn('class_section_id', $relaxedSectionIds); } else { $query->where('teacher_id', (int) $user->id); } } } elseif (! empty($filters['teacher_id'])) { $query->where('teacher_id', (int) $filters['teacher_id']); } if (! empty($filters['class_section_id'])) { $query->whereIn( 'class_section_id', $filterSectionIds !== [] ? $filterSectionIds : [(int) $filters['class_section_id']] ); } if (! empty($filters['subject'])) { $query->where('subject', (string) $filters['subject']); } if (! empty($filters['status'])) { $query->where('status', (string) $filters['status']); } $schoolYear = $this->resolveSchoolYearFilter($filters); $schoolYearRange = $this->schoolYearDateRange($schoolYear); if ($schoolYear !== '' && Schema::hasColumn('class_progress_reports', 'school_year')) { $query->where(function ($yearQuery) use ($schoolYear, $schoolYearRange) { $yearQuery->where('class_progress_reports.school_year', $schoolYear) ->orWhere(function ($legacyQuery) use ($schoolYear, $schoolYearRange) { $legacyQuery->where(function ($blankQuery) { $blankQuery->whereNull('class_progress_reports.school_year') ->orWhere('class_progress_reports.school_year', ''); }); if ($schoolYearRange !== null) { $legacyQuery->whereBetween('class_progress_reports.week_start', [ $schoolYearRange['start'], $schoolYearRange['end'], ]); } else { $legacyQuery->whereExists(function ($sectionQuery) use ($schoolYear) { $sectionQuery->select(DB::raw(1)) ->from('classSection as progress_filter_cs') ->whereColumn('progress_filter_cs.class_section_id', 'class_progress_reports.class_section_id') ->where('progress_filter_cs.school_year', $schoolYear); }); } }); }); } if (! empty($filters['semester']) && Schema::hasColumn('class_progress_reports', 'semester')) { $semester = (string) $filters['semester']; $query->where(function ($semesterQuery) use ($semester) { $semesterQuery->where('class_progress_reports.semester', $semester) ->orWhere(function ($legacyQuery) use ($semester) { $legacyQuery->where(function ($blankQuery) { $blankQuery->whereNull('class_progress_reports.semester') ->orWhere('class_progress_reports.semester', ''); })->whereExists(function ($sectionQuery) use ($semester) { $sectionQuery->select(DB::raw(1)) ->from('classSection as progress_filter_cs') ->whereColumn('progress_filter_cs.class_section_id', 'class_progress_reports.class_section_id') ->where('progress_filter_cs.semester', $semester); }); }); }); } if (! empty($filters['week_start'])) { $query->whereDate('week_start', '>=', $filters['week_start']); } if (! empty($filters['week_end'])) { $query->whereDate('week_end', '<=', $filters['week_end']); } $perPage = (int) ($filters['per_page'] ?? 20); return $query->paginate($perPage); } public function getReport(User $user, int $reportId): ClassProgressReport { $report = ClassProgressReport::query()->with('classSection')->findOrFail($reportId); if ($this->isAdmin($user)) { return $report; } if ($this->viewerMayAccessReport($user, $report)) { return $report; } abort(404); } public function weeklyReports(User $user, ClassProgressReport $report): array { $query = ClassProgressReport::query() ->with('classSection') ->where('class_section_id', $report->class_section_id) ->whereDate('week_start', $report->week_start) ->orderBy('subject', 'asc'); // Non-admins reach this only after policy `view` passed on the anchor report (author or teaches section). $rows = $query->get(); $attachments = $this->attachmentMap($rows->pluck('id')->all()); return $rows->map(function (ClassProgressReport $row) use ($attachments) { $row->setAttribute('status_label', $this->statusLabel($row->status)); $row->setAttribute('attachments', $attachments[$row->id] ?? []); return $row; })->all(); } public function attachmentMap(array $reportIds): array { $reportIds = array_values(array_filter(array_map('intval', $reportIds))); if ($reportIds === []) { return []; } $rows = ClassProgressAttachment::query() ->whereIn('report_id', $reportIds) ->orderBy('id', 'asc') ->get(); $map = []; foreach ($rows as $row) { $map[$row->report_id][] = [ 'id' => $row->id, 'name' => $row->original_name ?: basename((string) $row->file_path), 'file_path' => $row->file_path, ]; } return $map; } public function teacherAssignments(User $user, ?string $schoolYear, ?string $semester): array { if ($schoolYear === null || $schoolYear === '') { $schoolYear = (string) (config('school.school_year') ?? ''); } $assignments = $this->teacherAssignmentsForTerm($user, $schoolYear !== '' ? $schoolYear : null); if ($assignments !== []) { return $assignments; } return $this->teacherAssignmentsForTerm($user, null); } public function meta(?string $schoolYear = null, ?string $semester = null): array { return $this->schoolYears->options($schoolYear, $semester); } private function resolveSchoolYearFilter(array $filters): string { return trim((string) ($filters['school_year'] ?? '')); } /** * @return array{start:string,end:string}|null */ private function schoolYearDateRange(string $schoolYear): ?array { $schoolYear = trim($schoolYear); if ($schoolYear === '') { return null; } if (Schema::hasTable('school_years')) { try { $row = DB::table('school_years') ->where('name', $schoolYear) ->first(['start_date', 'end_date']); if ($row && $row->start_date && $row->end_date) { return [ 'start' => substr((string) $row->start_date, 0, 10), 'end' => substr((string) $row->end_date, 0, 10), ]; } } catch (\Throwable) { } } if (preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $matches) === 1) { return [ 'start' => $matches[1].'-08-01', 'end' => $matches[2].'-07-31', ]; } return null; } private function statusLabel(?string $status): string { $options = (array) config('progress.status_options', []); return $options[$status] ?? 'Unknown'; } /** * Resolve the logged-in teacher's real assignment rows. The join accepts both * legacy section identifiers used in this codebase. * * @return list> */ private function teacherAssignmentsForTerm(User $user, ?string $schoolYear): array { if (! Schema::hasTable('classSection') || ! Schema::hasTable('teacher_class')) { return []; } $teacherClassHasSchoolYear = Schema::hasColumn('teacher_class', 'school_year'); $teacherClassHasSemester = Schema::hasColumn('teacher_class', 'semester'); $schoolYearSelect = $teacherClassHasSchoolYear ? 'COALESCE(NULLIF(tc.school_year, ""), cs_code.school_year, cs_pk.school_year) AS school_year' : 'COALESCE(cs_code.school_year, cs_pk.school_year) AS school_year'; $semesterSelect = $teacherClassHasSemester ? 'COALESCE(NULLIF(tc.semester, ""), cs_code.semester, cs_pk.semester) AS semester' : 'COALESCE(cs_code.semester, cs_pk.semester) AS semester'; $query = DB::table('teacher_class as tc') ->selectRaw(" COALESCE(cs_code.class_section_name, cs_pk.class_section_name) AS class_section_name, COALESCE(cs_code.id, cs_pk.id) AS class_section_pk, COALESCE(cs_code.class_section_id, cs_pk.class_section_id) AS class_section_id, c.id AS class_id, c.class_name, tc.teacher_id, tc.position, {$schoolYearSelect}, {$semesterSelect} ") ->leftJoin('classSection as cs_code', 'tc.class_section_id', '=', 'cs_code.class_section_id') ->leftJoin('classSection as cs_pk', function ($join) { $join->on('tc.class_section_id', '=', 'cs_pk.id') ->whereNull('cs_code.class_section_id'); }) ->leftJoin('classes as c', DB::raw('COALESCE(cs_code.class_id, cs_pk.class_id)'), '=', 'c.id') ->where('tc.teacher_id', (int) $user->id) ->whereNotNull('tc.class_section_id') ->whereRaw('COALESCE(cs_code.class_section_id, cs_pk.class_section_id) IS NOT NULL'); if ($schoolYear !== null && $schoolYear !== '') { if ($teacherClassHasSchoolYear) { $query->where(function ($yearQuery) use ($schoolYear) { $yearQuery->where('tc.school_year', $schoolYear) ->orWhere(function ($legacyYearQuery) use ($schoolYear) { $legacyYearQuery->where(function ($blankYearQuery) { $blankYearQuery->whereNull('tc.school_year') ->orWhere('tc.school_year', ''); })->whereRaw('COALESCE(cs_code.school_year, cs_pk.school_year) = ?', [$schoolYear]); }); }); } else { $query->whereRaw('COALESCE(cs_code.school_year, cs_pk.school_year) = ?', [$schoolYear]); } } return $query ->orderByRaw("CASE tc.position WHEN 'main' THEN 0 WHEN 'ta' THEN 1 ELSE 2 END") ->orderByRaw('COALESCE(cs_code.class_section_name, cs_pk.class_section_name) ASC') ->get() ->map(fn ($row) => [ 'class_section_pk' => isset($row->class_section_pk) ? (int) $row->class_section_pk : null, 'class_section_id' => (int) $row->class_section_id, 'class_section_name' => (string) $row->class_section_name, 'class_id' => isset($row->class_id) ? (int) $row->class_id : null, 'class_name' => $row->class_name ?? null, 'teacher_id' => (int) $row->teacher_id, 'teacher_role' => ucfirst((string) $row->position), 'school_year' => $row->school_year ?? null, 'semester' => $row->semester ?? null, ]) ->unique('class_section_id') ->values() ->all(); } /** * @return list */ private function equivalentClassSectionIds(int $classSectionId): array { if ($classSectionId <= 0) { return []; } $ids = [$classSectionId => true]; if (! Schema::hasTable('classSection')) { return [$classSectionId]; } $rows = DB::table('classSection') ->select('id', 'class_section_id') ->where('class_section_id', $classSectionId) ->orWhere('id', $classSectionId) ->get(); foreach ($rows as $row) { foreach ([(int) $row->id, (int) $row->class_section_id] as $id) { if ($id > 0) { $ids[$id] = true; } } } $ids = array_map('intval', array_keys($ids)); sort($ids); return $ids; } private function isAdmin(User $user): bool { $roles = $user->roles() ->pluck('roles.name') ->map(fn ($name) => strtolower((string) $name)) ->toArray(); foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $role) { if (in_array($role, $roles, true)) { return true; } } return false; } private function isParent(User $user): bool { return $user->roles() ->pluck('roles.name') ->map(fn ($name) => strtolower((string) $name)) ->contains('parent'); } /** * @return list */ private function parentSectionIds(int $parentId): array { if ($parentId <= 0 || ! Schema::hasTable('enrollments')) { return []; } return DB::table('enrollments') ->where('parent_id', $parentId) ->where('is_withdrawn', 0) ->whereNotNull('class_section_id') ->distinct() ->pluck('class_section_id') ->map(fn ($id) => (int) $id) ->filter(fn ($id) => $id > 0) ->values() ->all(); } /** * legacy `ClassProgressController::resolveAssignedTeacherIds`. * * @return list */ public function resolveAssignedTeacherIds(int $classSectionId): array { $schoolYear = (string) (Configuration::getConfig('school_year') ?? ''); if ($schoolYear === '' || $classSectionId <= 0) { return []; } $semester = (string) (Configuration::getConfig('semester') ?? ''); $rows = TeacherClass::assignedForSectionTerm($classSectionId, $semester, $schoolYear); if ($rows === []) { $rows = TeacherClass::assignedForSectionTerm($classSectionId, $semester, $schoolYear, null, false); } $ids = []; foreach ($rows as $row) { $id = (int) ($row['teacher_id'] ?? 0); if ($id > 0) { $ids[$id] = true; } } return array_map('intval', array_keys($ids)); } private function viewerMayAccessReport(User $user, ClassProgressReport $report): bool { if ((int) $report->teacher_id === (int) $user->id) { return true; } return in_array( (int) $report->class_section_id, TeacherClass::distinctAccessibleSectionIdsForTeacher((int) $user->id), true ); } }