get('user_id') ?? 0); } /** * Upsert by (user_id, date, semester, school_year). */ public function upsertOne( int $userId, ?string $roleName, string $date, string $semester, string $schoolYear, string $status, ?string $reason, ?int $editorId = null ): bool { $existing = $this->where([ 'user_id' => $userId, 'date' => $date, 'semester' => $semester, 'school_year' => $schoolYear, ])->first(); $row = [ 'user_id' => $userId, 'role_name' => $roleName, 'date' => $date, 'semester' => $semester, 'school_year' => $schoolYear, 'status' => $status, 'reason' => $reason ?: null, 'updated_by' => $editorId ?? $this->currentEditorId(), ]; if ($existing) { return $this->update((int)$existing['id'], $row); } $row['created_by'] = $editorId ?? $this->currentEditorId(); return $this->insert($row) !== false; } // Optional: helpers/constants public const STATUS_PRESENT = 'present'; public const STATUS_ABSENT = 'absent'; public const STATUS_LATE = 'late'; public const POS_MAIN = 'main'; public const POS_TA = 'ta'; /** Normalize/validate a status string; return null for empty/invalid (means delete). */ protected function normStatus(?string $status): ?string { $s = strtolower((string) $status); return in_array($s, [self::STATUS_PRESENT, self::STATUS_ABSENT, self::STATUS_LATE], true) ? $s : null; } /** Quick session user id (safe). */ protected function currentUserId(): ?int { $session = session(); return $session ? ($session->get('user_id') ?: null) : null; } /** * Upsert a single cell by (user_id, date). * - If $status is empty/invalid => delete the record for that day (if exists). * - Otherwise upsert with provided fields. * * @param int $userId * @param string $date Y-m-d * @param string|null $status present|absent|late or ''/null to delete * @param string $semester * @param string $schoolYear * @param string|null $roleName * @param int|null $classSectionId * @param string|null $position main|ta (nullable for non-teachers) * @param string|null $reason */ public function upsertCell( int $userId, string $date, ?string $status, string $semester, string $schoolYear, ?string $roleName = null, ?int $classSectionId = null, ?string $position = null, ?string $reason = null ): bool { if ($userId <= 0 || !$this->isValidDate($date) || $semester === '' || $schoolYear === '') { return false; } $existing = $this->where(['user_id' => $userId, 'date' => $date])->first(); $norm = $this->normStatus($status); // DELETE if status cleared if ($norm === null) { if ($existing) { return (bool) $this->delete((int)$existing['id']); } return true; // nothing to delete } // UPSERT $row = [ 'user_id' => $userId, 'role_name' => $roleName ?? ($existing['role_name'] ?? null), 'class_section_id' => $classSectionId, 'position' => $position, 'date' => $date, 'semester' => $semester, 'school_year' => $schoolYear, 'status' => $norm, 'reason' => $reason, 'updated_by' => $this->currentUserId(), ]; if ($existing) { $row['id'] = (int)$existing['id']; return (bool) $this->save($row); } $row['created_by'] = $this->currentUserId(); return $this->insert($row) !== false; } /** * Return all staff (non Parent/Guest) with their status for a given day/term. * If a user has multiple roles, this picks a stable one via MIN(r.name). */ public function staffWithStatusForDay(string $date, string $semester, string $schoolYear, array $excludeUserIds = []): array { $db = $this->db; // Subquery to pick one role per user (MIN by name for stability) $roleSub = $db->table('user_roles ur') ->select('ur.user_id, MIN(r.name) AS role_name') ->join('roles r', 'r.id = ur.role_id', 'left') ->groupBy('ur.user_id'); $builder = $db->table('users u') ->select(" u.id AS user_id, u.firstname, u.lastname, COALESCE(rs.role_name, '') AS role_name, sa.id AS satt_id, sa.status, sa.reason ") ->joinSubquery($roleSub, 'rs', 'rs.user_id = u.id', 'left') ->join('staff_attendance sa', "sa.user_id = u.id AND sa.date = {$db->escape($date)} AND sa.semester = {$db->escape($semester)} AND sa.school_year = {$db->escape($schoolYear)}", 'left') ->whereNotIn('LOWER(rs.role_name)', ['parent', 'guest']) ->orderBy('u.firstname', 'ASC') ->orderBy('u.lastname', 'ASC'); if (!empty($excludeUserIds)) { $builder->whereNotIn('u.id', $excludeUserIds); } return $builder->get()->getResultArray(); } // ---------- helpers ---------- protected function isValidDate(string $d): bool { $dt = date_create_from_format('Y-m-d', $d); return $dt && $dt->format('Y-m-d') === $d; } public function getOne(int $userId, string $date): ?array { return $this->where(['user_id' => $userId, 'date' => $date])->first() ?: null; } public function deleteOne(int $userId, string $date): bool { $row = $this->getOne($userId, $date); return $row ? (bool)$this->delete((int)$row['id']) : true; } /* ========================= * TEACHER/TA HELPERS (staff_attendance-backed) * ========================= */ /** Return assigned teachers (main + TAs) for a section/term, with names. */ public function getAssignedTeachersForSection(int $classSectionId, string $semester, string $schoolYear): array { return $this->db->table('teacher_class tc') ->select(" tc.class_section_id, tc.teacher_id AS teacher_id, tc.position, u.firstname, u.lastname ", false) ->join('users u', 'u.id = tc.teacher_id', 'inner') ->where('tc.class_section_id', $classSectionId) ->where('tc.school_year', $schoolYear) ->orderBy("FIELD(tc.position,'main','ta')", '', false) ->orderBy('u.firstname', 'ASC') ->get()->getResultArray(); } /** Get teacher/TA status for a given day/term in staff_attendance. */ public function getStatusForDay(int $teacherId, int $classSectionId, string $date, string $semester, string $schoolYear): ?array { return $this->asArray() ->where('user_id', $teacherId) ->where('class_section_id', $classSectionId) // SECTION CODE ->where('date', $date) ->where('semester', $semester) ->where('school_year', $schoolYear) ->first() ?: null; } public function assignedWithStatusForDay(int $classSectionId, string $date, string $semester, string $schoolYear): array { $db = $this->db; $join = "t.user_id = tc.teacher_id AND t.class_section_id = tc.class_section_id AND t.date = " . $db->escape($date) . " AND t.semester = " . $db->escape($semester) . " AND t.school_year = " . $db->escape($schoolYear); return $db->table('teacher_class tc') ->select(" tc.class_section_id, tc.teacher_id AS teacher_id, tc.position, u.firstname, u.lastname, t.id AS satt_id, t.status, t.reason ", false) ->join('users u', 'u.id = tc.teacher_id', 'inner') ->join($this->table . ' t', $join, 'left') // staff_attendance ->where('tc.class_section_id', $classSectionId) ->where('tc.school_year', $schoolYear) ->orderBy("FIELD(tc.position,'main','ta')", '', false) ->orderBy('u.firstname', 'ASC') ->get()->getResultArray(); } /** True if every assigned teacher has a status row for this day/term (in staff_attendance). */ public function allAssignedStatusesPresent(int $classSectionId, string $date, string $semester, string $schoolYear): bool { // Count assigned teachers (from teacher_class) $assigned = $this->db->table('teacher_class') ->where('class_section_id', $classSectionId) // SECTION CODE ->where('school_year', $schoolYear) ->countAllResults(); if ($assigned <= 0) return true; // Count distinct users saved in staff_attendance for that day/term/section $present = (int)($this->db->table($this->table) ->select('COUNT(DISTINCT user_id) AS c', false) ->where('class_section_id', $classSectionId) ->where('date', $date) ->where('semester', $semester) ->where('school_year', $schoolYear) ->get()->getRowArray()['c'] ?? 0); return $present === (int)$assigned; } /** * Assigned teachers for a section/term with attendance for a specific date. * Returns: class_section_id, teacher_id, position, firstname, lastname, satt_id, status, reason */ /** * Group assignments by section (SECTION CODE) for a term. * Returns: [class_section_id => [ ['teacher_id'=>..,'position'=>'main|ta','firstname'=>..,'lastname'=>..], ... ]] */ public function assignedByTerm(string $semester, string $schoolYear): array { $rows = $this->db->table('teacher_class tc') ->select('tc.class_section_id, tc.teacher_id AS teacher_id, tc.position, u.firstname, u.lastname') ->join('users u', 'u.id = tc.teacher_id', 'inner') ->where('tc.school_year', $schoolYear) ->orderBy("FIELD(tc.position,'main','ta')", '', false) ->orderBy('u.firstname', 'ASC') ->get()->getResultArray(); $out = []; foreach ($rows as $r) { $sid = (int)$r['class_section_id']; $out[$sid][] = $r; } return $out; } /** * Resolve assignment bundles with a resolved section PK and label. * Shape: [$requestedCode] = ['resolved_id'=>int,'label'=>string,'teachers'=>[...]] */ public function assignedByTermResolved(string $semester, string $schoolYear): array { $db = $this->db; // 1) PK matches (tc.class_section_id == cs.id) $pkRows = $db->table('teacher_class tc') // ...inside assignedByTermResolved ->select(" tc.class_section_id AS requested_section_id, cs.id AS resolved_section_id, cs.class_section_name AS section_label, tc.teacher_id AS teacher_id, tc.position, u.firstname, u.lastname ", false) ->join('users u', 'u.id = tc.teacher_id', 'inner') ->join('classSection cs', 'cs.id = tc.class_section_id', 'inner') ->where('tc.school_year', $schoolYear) ->get()->getResultArray(); $out = []; $pkTaken = []; foreach ($pkRows as $r) { $req = (int)($r['requested_section_id'] ?? 0); if (!isset($out[$req])) { $out[$req] = [ 'resolved_id' => (int)$r['resolved_section_id'], 'label' => trim((string)$r['section_label']), 'teachers' => [], ]; $pkTaken[$req] = true; } $out[$req]['teachers'][] = [ 'teacher_id' => (int)$r['teacher_id'], 'position' => (string)$r['position'], 'firstname' => (string)($r['firstname'] ?? ''), 'lastname' => (string)($r['lastname'] ?? ''), ]; } // 2) CODE matches (tc.class_section_id == cs.class_section_id) if not matched by PK $codeRows = $db->table('teacher_class tc') ->select(" tc.class_section_id AS requested_section_id, cs.id AS resolved_section_id, cs.class_section_name AS section_label, tc.user_id AS teacher_id, tc.position, u.firstname, u.lastname ", false) ->join('users u', 'u.id = tc.user_id', 'inner') ->join('classSection cs', 'cs.class_section_id = tc.class_section_id', 'inner') ->where('tc.school_year', $schoolYear) ->get()->getResultArray(); foreach ($codeRows as $r) { $req = (int)($r['requested_section_id'] ?? 0); if (isset($pkTaken[$req])) continue; if (!isset($out[$req])) { $out[$req] = [ 'resolved_id' => (int)$r['resolved_section_id'], 'label' => trim((string)$r['section_label']), 'teachers' => [], ]; } $out[$req]['teachers'][] = [ 'teacher_id' => (int)$r['teacher_id'], 'position' => (string)$r['position'], 'firstname' => (string)($r['firstname'] ?? ''), 'lastname' => (string)($r['lastname'] ?? ''), ]; } foreach ($out as $req => &$bundle) { if (empty($bundle['resolved_id'])) $bundle['resolved_id'] = (int)$req; if ($bundle['label'] === '') $bundle['label'] = 'Section #' . (int)$req; } unset($bundle); return $out; } /** Group assignments by SECTION CODE (uses tc.class_section_id). */ public function assignedByTermByCode(string $semester, string $schoolYear): array { $rows = $this->db->table('teacher_class tc') ->select('tc.class_section_id, tc.teacher_id AS teacher_id, tc.position, u.firstname, u.lastname') ->join('users u', 'u.id = tc.teacher_id', 'inner') ->where('tc.school_year', $schoolYear) ->orderBy('tc.class_section_id', 'ASC') ->orderBy("FIELD(tc.position,'main','ta')", '', false) ->orderBy('u.firstname', 'ASC') ->get()->getResultArray(); $out = []; foreach ($rows as $r) { $code = (int)$r['class_section_id']; $out[$code][] = [ 'teacher_id' => (int)$r['teacher_id'], 'position' => (string)$r['position'], 'firstname' => (string)($r['firstname'] ?? ''), 'lastname' => (string)($r['lastname'] ?? ''), ]; } return $out; } /** Assigned + status for a single day by SECTION CODE (joins staff_attendance). */ public function assignedWithStatusForDayByCode( int $sectionCode, string $date, string $semester, string $schoolYear ): array { $db = $this->db; $join = "t.user_id = tc.teacher_id AND t.class_section_id = tc.class_section_id AND t.date = " . $db->escape($date) . " AND t.semester = " . $db->escape($semester) . " AND t.school_year = " . $db->escape($schoolYear); return $db->table('teacher_class tc') ->select(" tc.class_section_id, tc.teacher_id AS teacher_id, tc.position, u.firstname, u.lastname, t.id AS satt_id, t.status, t.reason ", false) ->join('users u', 'u.id = tc.teacher_id', 'inner') ->join($this->table . ' t', $join, 'left') // staff_attendance ->where('tc.class_section_id', $sectionCode) ->where('tc.school_year', $schoolYear) ->orderBy("FIELD(tc.position,'main','ta')", '', false) ->orderBy('u.firstname', 'ASC') ->get()->getResultArray(); } /** * Statuses for a range (by SECTION CODE) from staff_attendance. * Returns: [sectionCode][teacherId][Y-m-d] = ['status'=>..,'reason'=>..] */ public function statusesByRange(array $sectionCodes, string $startDate, string $endDate, string $semester, string $schoolYear): array { if (empty($sectionCodes)) return []; $rows = $this->db->table($this->table) // staff_attendance ->select('class_section_id, user_id, DATE_FORMAT(date,"%Y-%m-%d") AS d, status, reason', false) ->whereIn('class_section_id', $sectionCodes) // SECTION CODE ->where('date >=', $startDate) ->where('date <=', $endDate) ->where('semester', $semester) ->where('school_year', $schoolYear) ->get()->getResultArray(); $map = []; foreach ($rows as $r) { $sid = (int)$r['class_section_id']; $tid = (int)$r['user_id']; $day = (string)$r['d']; $map[$sid][$tid][$day] = [ 'status' => strtolower((string)$r['status']), 'reason' => $r['reason'], ]; } return $map; } }