Files
alrahma_sunday_school/app/Models/StaffAttendanceModel.php
T
2026-05-16 13:44:12 -04:00

507 lines
18 KiB
PHP
Executable File

<?php
namespace App\Models;
use CodeIgniter\Model;
use Config\Services;
class StaffAttendanceModel extends Model
{
protected $table = 'staff_attendance';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $useAutoIncrement = true;
// ✅ Only columns that really exist in staff_attendance
protected $allowedFields = [
'user_id',
'role_name',
'date',
'semester',
'school_year',
'status', // enum('present','absent','late')
'reason',
'created_at',
'updated_at',
'created_by',
'updated_by',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
// (Optional helper, keep if you like)
public function currentEditorId(): int
{
return (int) (session()->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;
}
}