builder() ->select('id') ->where('class_section_id', $classSectionId) ->where('date', $date) ->groupStart() ->where('status', 'published') ->orWhere('status', 'finalized') // tolerate legacy data ->groupEnd(); if ($semester !== null) $builder->where('semester', $semester); if ($schoolYear !== null) $builder->where('school_year', $schoolYear); return (bool) $builder->get()->getFirstRow(); } /** * Find a day row by (section, date, term). Returns array|null. */ public function findBySectionDate(int $classSectionId, string $date, ?string $semester, ?string $schoolYear): ?array { return $this->where([ 'class_section_id' => $classSectionId, 'date' => $date, 'semester' => $semester, 'school_year' => $schoolYear, ])->first() ?: null; } /** * Get the draft row for (section,date,term), creating it if missing. * Relies on the unique key (class_section_id, date, semester, school_year) to prevent duplicates. */ public function getOrCreateDraft( int $classSectionId, string $date, ?string $semester, ?string $schoolYear, int $userId = 0 ): array { // Try existing $row = $this->findBySectionDate($classSectionId, $date, $semester, $schoolYear); if ($row) return $row; $now = utc_now(); $data = [ 'class_section_id' => $classSectionId, 'date' => $date, 'status' => 'draft', 'submitted_by' => null, 'submitted_at' => null, 'published_by' => null, 'published_at' => null, 'auto_publish_at' => null, 'reopened_by' => null, 'reopened_at' => null, 'reopen_reason' => null, 'semester' => $semester, 'school_year' => $schoolYear, 'created_at' => $now, 'updated_at' => $now, ]; try { $id = $this->insert($data, true); return $this->find($id); } catch (\Throwable $e) { // Race: someone else inserted the same unique key. Fetch it. $row = $this->findBySectionDate($classSectionId, $date, $semester, $schoolYear); if ($row) return $row; throw $e; } } /** * DEPRECATED: Prior code used "finalize" to lock immediately. * In the new workflow, "finalize" means "teacher submit" * (locks teachers, keeps admins editable). Use submit() or publish(). */ public function finalize(int $id, int $userId): bool { // Alias to submit() to avoid hard-locking admins by legacy calls. return $this->submit($id, $userId, null); } /** * Teacher submit: status => submitted (teacher locked, admin editable). * Optionally set auto_publish_at (controller can compute second-Sunday rule). */ public function submit(int $id, int $userId, ?string $autoPublishAt = null): bool { $now = utc_now(); return (bool) $this->update($id, [ 'status' => 'submitted', 'submitted_by' => $userId, 'submitted_at' => $now, 'auto_publish_at' => $autoPublishAt, 'updated_at' => $now, ]); } /** * Admin publish (hard lock): status => published. */ public function publish(int $id, int $userId): bool { $now = utc_now(); return (bool) $this->update($id, [ 'status' => 'published', 'published_by' => $userId, 'published_at' => $now, 'updated_at' => $now, ]); } /** * Admin reopen a submitted/published day back to 'draft' (default) or 'submitted'. * Provide an optional reason for audit. */ public function reopen(int $id, int $userId, string $toStatus = 'draft', ?string $reason = null): bool { if (!in_array($toStatus, ['draft', 'submitted'], true)) { throw new \InvalidArgumentException('toStatus must be "draft" or "submitted".'); } $now = utc_now(); return (bool) $this->update($id, [ 'status' => $toStatus, 'reopened_by' => $userId, 'reopened_at' => $now, 'reopen_reason' => $reason, 'updated_at' => $now, ]); } }