policyResolver = $this->policyResolver ?? new GradingPolicyResolver(); $this->finalizationValidator = $this->finalizationValidator ?? new GradebookFinalizationValidator(); } public function toggle(int $classSectionId, string $semester, string $schoolYear, ?int $userId): bool { if ($classSectionId <= 0 || $semester === '' || $schoolYear === '') { throw new RuntimeException('Missing class section or term.'); } $existing = GradingLock::getLock($classSectionId, $semester, $schoolYear); if ($existing && $existing->is_locked) { $existing->update([ 'is_locked' => 0, 'locked_by' => null, 'locked_at' => null, ]); return false; } $this->assertCanLock($classSectionId, $semester, $schoolYear); if ($existing) { $existing->update([ 'is_locked' => 1, 'locked_by' => $userId ?: null, 'locked_at' => now(), ]); } else { GradingLock::query()->create([ 'class_section_id' => $classSectionId, 'semester' => $semester, 'school_year' => $schoolYear, 'is_locked' => 1, 'locked_by' => $userId ?: null, 'locked_at' => now(), ]); } return true; } public function lockAll(string $semester, string $schoolYear, ?int $userId): int { if ($semester === '' || $schoolYear === '') { throw new RuntimeException('Missing semester or school year.'); } $sectionIds = ClassSection::query() ->select('class_section_id') ->groupBy('class_section_id') ->pluck('class_section_id') ->map(fn ($v) => (int) $v) ->filter(fn ($v) => $v > 0) ->values() ->all(); if (empty($sectionIds)) { return 0; } $existingLocks = GradingLock::query() ->whereIn('class_section_id', $sectionIds) ->where('semester', $semester) ->where('school_year', $schoolYear) ->get(); $existingBySection = []; foreach ($existingLocks as $row) { $sid = (int) $row->class_section_id; if ($sid > 0) { $existingBySection[$sid] = $row; } } $now = now(); $insertRows = []; $count = 0; foreach ($sectionIds as $sid) { $this->assertCanLock($sid, $semester, $schoolYear); if (!empty($existingBySection[$sid])) { if ($existingBySection[$sid]->is_locked) { continue; } $existingBySection[$sid]->update([ 'is_locked' => 1, 'locked_by' => $userId ?: null, 'locked_at' => $now, ]); $count++; continue; } $insertRows[] = [ 'class_section_id' => $sid, 'semester' => $semester, 'school_year' => $schoolYear, 'is_locked' => 1, 'locked_by' => $userId ?: null, 'locked_at' => $now, 'created_at' => $now, 'updated_at' => $now, ]; $count++; } if (!empty($insertRows)) { GradingLock::query()->insert($insertRows); } return $count; } private function assertCanLock(int $classSectionId, string $semester, string $schoolYear): void { $mode = $this->policyResolver->calculationMode($classSectionId, $semester, $schoolYear); if ($mode !== GradingPolicyResolver::STRONG_MODE) { return; } $result = $this->finalizationValidator->validateClassSectionBeforeLock($classSectionId, $semester, $schoolYear); if (!$result['valid']) { $count = count($result['errors']); throw new RuntimeException("Cannot lock class section {$classSectionId}; strong grading validation failed with {$count} error(s)."); } } }