Files
alrahma_sunday_school_api/app/Models/StaffAttendance.php
T
2026-06-09 00:03:03 -04:00

485 lines
16 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\DB;
class StaffAttendance extends BaseModel
{
protected $table = 'staff_attendance';
public $timestamps = true;
/**
* IMPORTANT:
* Your earlier comment said only these columns exist.
* But later methods use class_section_id + position.
*
* ✅ If class_section_id/position DO exist in your DB, add them to fillable.
* ❌ If they do NOT exist, remove them and also remove related methods/scopes.
*/
protected $fillable = [
'user_id',
'role_name',
'date',
'semester',
'school_year',
'status',
'present',
'absent',
'late',
'reason',
'created_at',
'updated_at',
'created_by',
'updated_by',
];
protected $casts = [
'user_id' => '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'],
];
}
}