471 lines
16 KiB
PHP
471 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use App\Models\BaseModel;
|
|
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 CI 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'],
|
|
];
|
|
}
|
|
} |