Files
alrahma_sunday_school_api/app/Services/ClassProgress/ClassProgressQueryService.php
T
root 031e499819
API CI/CD / Validate (composer + pint) (push) Successful in 3m10s
API CI/CD / Test (PHPUnit) (push) Failing after 6m49s
API CI/CD / Build frontend assets (push) Successful in 1m3s
API CI/CD / Security audit (push) Failing after 1m0s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
fix apply the plan docs/alrahma_api_fix_plan_school_year_only_v7
2026-07-07 01:52:29 -04:00

347 lines
12 KiB
PHP

<?php
namespace App\Services\ClassProgress;
use App\Models\ClassProgressAttachment;
use App\Models\ClassProgressReport;
use App\Models\Configuration;
use App\Models\TeacherClass;
use App\Models\User;
use App\Services\SchoolYears\SchoolYearContextService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class ClassProgressQueryService
{
private SchoolYearContextService $schoolYears;
public function __construct($schoolYears = null)
{
$this->schoolYears = $schoolYears instanceof SchoolYearContextService
? $schoolYears
: app(SchoolYearContextService::class);
}
public function listReports(User $user, array $filters): LengthAwarePaginator
{
$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::distinctSectionIdsForTeacher((int) $user->id);
if (! empty($filters['class_section_id'])) {
$sid = (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 (! in_array($sid, $relaxedSectionIds, true)) {
$query->where('teacher_id', (int) $user->id);
}
} else {
$query->where(function ($q) use ($user, $relaxedSectionIds) {
$q->where('teacher_id', (int) $user->id);
if ($relaxedSectionIds !== []) {
$q->orWhereIn('class_section_id', $relaxedSectionIds);
}
});
}
}
} elseif (! empty($filters['teacher_id'])) {
$query->where('teacher_id', (int) $filters['teacher_id']);
}
if (! empty($filters['class_section_id'])) {
$query->where('class_section_id', (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') ?? '');
}
return TeacherClass::getClassAssignmentsByUserId($user->id, $schoolYear, $semester);
}
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';
}
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<int>
*/
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<int>
*/
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::distinctSectionIdsForTeacher((int) $user->id),
true
);
}
}