'integer', 'class_section_id' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'date' => 'date:Y-m-d', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; /* ============================================================ * 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'; public static function allowedStatuses(): array { return [self::STATUS_PRESENT, self::STATUS_ABSENT, self::STATUS_LATE]; } /* ============================================================ * Relationships (optional) * ============================================================ */ public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); } public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); // change to Admin if needed } public function updater(): BelongsTo { return $this->belongsTo(User::class, 'updated_by'); // change to Admin if needed } public function classSection(): BelongsTo { return $this->belongsTo(ClassSection::class, 'class_section_id'); } /* ============================================================ * Scopes * ============================================================ */ public function scopeForDay(Builder $q, string $date): Builder { return $q->whereDate('date', $date); } public function scopeForTerm(Builder $q, string $semester, string $schoolYear): Builder { return $q->where('semester', $semester)->where('school_year', $schoolYear); } public function scopeForUser(Builder $q, int $userId): Builder { return $q->where('user_id', $userId); } public function scopeForSection(Builder $q, int $classSectionId): Builder { return $q->where('class_section_id', $classSectionId); } /* ============================================================ * Helpers * ============================================================ */ protected static function normStatus(?string $status): ?string { $s = strtolower(trim((string) $status)); return in_array($s, self::allowedStatuses(), true) ? $s : null; } protected static function isValidDate(string $d): bool { $dt = date_create_from_format('Y-m-d', $d); return $dt && $dt->format('Y-m-d') === $d; } /* ============================================================ * Upserts (Laravel style) * ============================================================ */ /** * Upsert by (user_id, date, semester, school_year) * Returns saved model or null if invalid. */ public static function upsertOne( int $userId, ?string $roleName, string $date, string $semester, string $schoolYear, string $status, ?string $reason, ?int $editorId = null ): ?self { if ($userId <= 0 || !self::isValidDate($date) || $semester === '' || $schoolYear === '') { return null; } $norm = self::normStatus($status); if ($norm === null) { return null; } $editorId = $editorId ?: (auth()->id() ?? null); $keys = [ 'user_id' => $userId, 'date' => $date, 'semester' => $semester, 'school_year' => $schoolYear, ]; $payload = [ 'role_name' => $roleName, 'status' => $norm, 'reason' => $reason ?: null, 'updated_by' => $editorId, ]; // Ensure created_by is set on create return static::query()->updateOrCreate($keys, $payload + [ 'created_by' => $editorId, ]); } /** * Upsert a single cell by (user_id, date). * - If status empty/invalid => delete record for that day (if exists) and return true. * - Otherwise upsert. */ public static function upsertCell( int $userId, string $date, ?string $status, string $semester, string $schoolYear, ?string $roleName = null, ?int $classSectionId = null, ?string $position = null, ?string $reason = null, ?int $editorId = null ): bool { if ($userId <= 0 || !self::isValidDate($date) || $semester === '' || $schoolYear === '') { return false; } $editorId = $editorId ?: (auth()->id() ?? null); $existing = static::query() ->where('user_id', $userId) ->whereDate('date', $date) ->first(); $norm = self::normStatus($status); // DELETE if status cleared if ($norm === null) { if ($existing) { return (bool) $existing->delete(); } return true; } $keys = [ 'user_id' => $userId, 'date' => $date, 'semester' => $semester, 'school_year' => $schoolYear, ]; $payload = [ 'role_name' => $roleName ?? ($existing->role_name ?? null), 'class_section_id' => $classSectionId, 'position' => $position, 'status' => $norm, 'reason' => $reason, 'updated_by' => $editorId, ]; static::query()->updateOrCreate($keys, $payload + ['created_by' => $editorId]); return true; } public static function getOne(int $userId, string $date): ?self { return static::query()->where('user_id', $userId)->whereDate('date', $date)->first(); } public static function deleteOne(int $userId, string $date): bool { $row = static::getOne($userId, $date); return $row ? (bool) $row->delete() : true; } /* ============================================================ * Staff list with status for a day (users + roles + staff_attendance) * Mirrors your legacy staffWithStatusForDay() * ============================================================ */ public static function staffWithStatusForDay( string $date, string $semester, string $schoolYear, array $excludeUserIds = [] ): array { // Subquery: pick one stable role name per user (MIN(name)) $roleSub = DB::table('user_roles as ur') ->selectRaw('ur.user_id, MIN(r.name) as role_name') ->leftJoin('roles as r', 'r.id', '=', 'ur.role_id') ->groupBy('ur.user_id'); $q = DB::table('users as u') ->selectRaw(" 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 ") ->leftJoinSub($roleSub, 'rs', fn ($j) => $j->on('rs.user_id', '=', 'u.id')) ->leftJoin('staff_attendance as sa', function ($j) use ($date, $semester, $schoolYear) { $j->on('sa.user_id', '=', 'u.id') ->whereDate('sa.date', '=', $date) ->where('sa.semester', '=', $semester) ->where('sa.school_year', '=', $schoolYear); }) ->whereNotIn(DB::raw('LOWER(COALESCE(rs.role_name,""))'), ['parent', 'guest']) ->orderBy('u.firstname') ->orderBy('u.lastname'); if (!empty($excludeUserIds)) { $q->whereNotIn('u.id', array_values(array_map('intval', $excludeUserIds))); } return $q->get()->map(function ($r) { return [ 'user_id' => (int) $r->user_id, 'firstname' => (string) ($r->firstname ?? ''), 'lastname' => (string) ($r->lastname ?? ''), 'role_name' => (string) ($r->role_name ?? ''), 'satt_id' => $r->satt_id ? (int) $r->satt_id : null, 'status' => $r->status ? (string) $r->status : null, 'reason' => $r->reason, ]; })->all(); } /* ============================================================ * Teacher/TA helpers (teacher_class-backed) * ============================================================ */ public static function getAssignedTeachersForSection(int $classSectionId, string $schoolYear): array { return DB::table('teacher_class as tc') ->selectRaw('tc.class_section_id, tc.teacher_id as teacher_id, tc.position, u.firstname, u.lastname') ->join('users as u', 'u.id', '=', 'tc.teacher_id') ->where('tc.class_section_id', $classSectionId) ->where('tc.school_year', $schoolYear) ->orderByRaw("FIELD(tc.position,'main','ta')") ->orderBy('u.firstname') ->get() ->map(fn ($r) => [ 'class_section_id' => (int) $r->class_section_id, 'teacher_id' => (int) $r->teacher_id, 'position' => (string) $r->position, 'firstname' => (string) ($r->firstname ?? ''), 'lastname' => (string) ($r->lastname ?? ''), ])->all(); } public static function getStatusForDay( int $teacherId, int $classSectionId, string $date, string $semester, string $schoolYear ): ?array { $row = static::query() ->where('user_id', $teacherId) ->where('class_section_id', $classSectionId) ->whereDate('date', $date) ->where('semester', $semester) ->where('school_year', $schoolYear) ->first(); return $row ? $row->toArray() : null; } public static function assignedWithStatusForDay( int $classSectionId, string $date, string $semester, string $schoolYear ): array { return DB::table('teacher_class as tc') ->selectRaw(" 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 ") ->join('users as u', 'u.id', '=', 'tc.teacher_id') ->leftJoin('staff_attendance as t', function ($j) use ($date, $semester, $schoolYear) { $j->on('t.user_id', '=', 'tc.teacher_id') ->on('t.class_section_id', '=', 'tc.class_section_id') ->whereDate('t.date', '=', $date) ->where('t.semester', '=', $semester) ->where('t.school_year', '=', $schoolYear); }) ->where('tc.class_section_id', $classSectionId) ->where('tc.school_year', $schoolYear) ->orderByRaw("FIELD(tc.position,'main','ta')") ->orderBy('u.firstname') ->get() ->map(fn ($r) => [ 'class_section_id' => (int) $r->class_section_id, 'teacher_id' => (int) $r->teacher_id, 'position' => (string) $r->position, 'firstname' => (string) ($r->firstname ?? ''), 'lastname' => (string) ($r->lastname ?? ''), 'satt_id' => $r->satt_id ? (int) $r->satt_id : null, 'status' => $r->status ? strtolower((string) $r->status) : null, 'reason' => $r->reason, ])->all(); } public static function allAssignedStatusesPresent( int $classSectionId, string $date, string $semester, string $schoolYear ): bool { $assigned = (int) DB::table('teacher_class') ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->count(); if ($assigned <= 0) return true; $present = (int) DB::table('staff_attendance') ->where('class_section_id', $classSectionId) ->whereDate('date', $date) ->where('semester', $semester) ->where('school_year', $schoolYear) ->distinct('user_id') ->count('user_id'); return $present === $assigned; } /** * Statuses for a range by section codes. * Returns: [sectionCode][teacherId][Y-m-d] = ['status'=>..,'reason'=>..] */ public static function statusesByRange( array $sectionCodes, string $startDate, string $endDate, string $semester, string $schoolYear ): array { $codes = array_values(array_unique(array_filter(array_map('intval', $sectionCodes)))); if (empty($codes)) return []; $rows = DB::table('staff_attendance') ->selectRaw('class_section_id, user_id, DATE_FORMAT(date,"%Y-%m-%d") as d, status, reason') ->whereIn('class_section_id', $codes) ->whereDate('date', '>=', $startDate) ->whereDate('date', '<=', $endDate) ->where('semester', $semester) ->where('school_year', $schoolYear) ->get(); $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; } /* ============================================================ * Optional validation helper * ============================================================ */ public static function rules(bool $updating = false): array { $req = $updating ? 'sometimes' : 'required'; return [ 'user_id' => [$req, 'integer', 'min:1', 'exists:users,id'], 'date' => [$req, 'date_format:Y-m-d'], 'semester' => [$req, 'string', 'max:20'], 'school_year' => [$req, 'string', 'max:20'], 'role_name' => ['nullable', 'string', 'max:80'], 'status' => ['nullable', 'string', 'in:' . implode(',', self::allowedStatuses())], 'reason' => ['nullable', 'string', 'max:2000'], 'class_section_id' => ['nullable', 'integer'], 'position' => ['nullable', 'string', 'in:' . implode(',', [self::POS_MAIN, self::POS_TA])], 'created_by' => ['nullable', 'integer'], 'updated_by' => ['nullable', 'integer'], ]; } }