recreate project
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class AdditionalChargeModel extends Model
|
||||
{
|
||||
protected $table = 'additional_charges';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = false;
|
||||
|
||||
protected $allowedFields = [
|
||||
'parent_id',
|
||||
'invoice_id',
|
||||
'school_year',
|
||||
'semester',
|
||||
'charge_type',
|
||||
'title',
|
||||
'description',
|
||||
'amount',
|
||||
'due_date',
|
||||
'status',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $validationRules = [
|
||||
'parent_id' => 'permit_empty|integer',
|
||||
'invoice_id' => 'permit_empty|integer',
|
||||
'school_year' => 'required|string|max_length[20]',
|
||||
'semester' => 'required|string|max_length[20]',
|
||||
'charge_type' => 'required|in_list[add,deduct]',
|
||||
'title' => 'required|string|min_length[2]|max_length[255]',
|
||||
'description' => 'permit_empty|string',
|
||||
'amount' => 'required|decimal',
|
||||
'due_date' => 'permit_empty|valid_date',
|
||||
'status' => 'required|in_list[pending,applied]',
|
||||
'created_by' => 'permit_empty|integer',
|
||||
];
|
||||
|
||||
public function byParentTerm(int $parentId, string $year, string $sem, ?string $status = null): array
|
||||
{
|
||||
$b = $this->where('parent_id', $parentId)
|
||||
->where('school_year', $year)
|
||||
->where('semester', $sem);
|
||||
if ($status !== null) $b->where('status', $status);
|
||||
return $b->orderBy('id', 'DESC')->findAll();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function markApplied($ids, int $invoiceId): bool
|
||||
{
|
||||
$ids = is_array($ids) ? $ids : [$ids];
|
||||
if (!$ids) return true;
|
||||
return (bool) $this->whereIn('id', array_map('intval', $ids))
|
||||
->set(['status' => 'applied', 'invoice_id' => $invoiceId])
|
||||
->update();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function listAllForTerm(
|
||||
string $schoolYear,
|
||||
string $semester,
|
||||
?string $status = null,
|
||||
?string $q = null,
|
||||
int $perPage = 50,
|
||||
bool $fallbackToYear = true
|
||||
) {
|
||||
// Helper to build the query with optional semester filter
|
||||
$build = function (?string $semFilter) use ($schoolYear, $status, $q) {
|
||||
$b = $this->select(
|
||||
"additional_charges.*,
|
||||
CONCAT(COALESCE(users.lastname, ''), ', ', COALESCE(users.firstname, '')) AS parent_name,
|
||||
invoices.invoice_number"
|
||||
)
|
||||
->join('users', 'users.id = additional_charges.parent_id', 'left')
|
||||
->join('invoices', 'invoices.id = additional_charges.invoice_id', 'left')
|
||||
->where('additional_charges.school_year', $schoolYear);
|
||||
|
||||
if ($semFilter !== null && $semFilter !== '') {
|
||||
$b->where('additional_charges.semester', $semFilter);
|
||||
}
|
||||
|
||||
if (!empty($status)) {
|
||||
$b->where('additional_charges.status', $status);
|
||||
}
|
||||
|
||||
if (!empty($q)) {
|
||||
$b->groupStart()
|
||||
->like('additional_charges.title', $q)
|
||||
->orLike('additional_charges.description', $q)
|
||||
->orLike('users.firstname', $q)
|
||||
->orLike('users.lastname', $q)
|
||||
->orLike('invoices.invoice_number', $q)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
return $b;
|
||||
};
|
||||
|
||||
// First attempt: year + semester
|
||||
$result = $build($semester)->orderBy('additional_charges.id', 'DESC')->paginate($perPage);
|
||||
|
||||
// Fallback: if no rows for this semester and allowed, retry year-only (helps when Spring data isn't entered yet)
|
||||
if ($fallbackToYear && empty($result) && $semester !== '') {
|
||||
$result = $build(null)->orderBy('additional_charges.id', 'DESC')->paginate($perPage);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class AdminNotificationSubjectModel extends Model
|
||||
{
|
||||
protected $table = 'admin_notification_subjects';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
|
||||
protected $allowedFields = [
|
||||
'admin_id',
|
||||
'subject',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class AttendanceCommentTemplateModel extends Model
|
||||
{
|
||||
protected $table = 'attendance_comment_template';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = false;
|
||||
protected $allowedFields = [
|
||||
'min_score',
|
||||
'max_score',
|
||||
'template_text',
|
||||
'is_active',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
public function getActiveTemplates(): array
|
||||
{
|
||||
return $this->where('is_active', 1)
|
||||
->orderBy('min_score', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class AttendanceDataModel extends Model
|
||||
{
|
||||
protected $table = 'attendance_data';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'class_id',
|
||||
'class_section_id',
|
||||
'school_id',
|
||||
'student_id',
|
||||
'date',
|
||||
'status',
|
||||
'reason',
|
||||
'reported',
|
||||
'is_reported',
|
||||
'is_notified',
|
||||
'semester',
|
||||
'school_year',
|
||||
'modified_by',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
private ?array $reportedColumns = null;
|
||||
|
||||
protected $validationRules = [
|
||||
'class_id' => 'required|is_natural_no_zero',
|
||||
'class_section_id'=> 'required|is_natural_no_zero',
|
||||
'student_id' => 'required|is_natural_no_zero',
|
||||
'status' => 'required|in_list[present,absent,late]',
|
||||
'is_reported' => 'required|in_list[no,yes]',
|
||||
'is_notified' => 'required|in_list[no,yes]',
|
||||
'date' => 'required|valid_date[Y-m-d]',
|
||||
'semester' => 'required',
|
||||
'school_year' => 'required',
|
||||
];
|
||||
/**
|
||||
* Retrieve attendance by class and section for a specific date.
|
||||
*
|
||||
* @param int $classId
|
||||
* @param int $classSectionId
|
||||
* @param string $date
|
||||
* @return array
|
||||
*/
|
||||
public function getAttendanceByClass($classId, $classSectionId, $date)
|
||||
{
|
||||
return $this->where([
|
||||
'class_id' => $classId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $date
|
||||
])->findAll();
|
||||
}
|
||||
|
||||
public function unreportedTermRows(array $studentIds, string $schoolYear, ?string $semester): array
|
||||
{
|
||||
$qb = $this->builder()
|
||||
->select('id, student_id, class_id, class_section_id, date, status, reason, school_year, semester, is_reported')
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($semester !== null && $semester !== '') {
|
||||
$qb->where('semester', $semester);
|
||||
}
|
||||
if (!empty($studentIds)) {
|
||||
$qb->whereIn('student_id', $studentIds);
|
||||
}
|
||||
|
||||
$this->applyUnreportedFilter($qb);
|
||||
|
||||
return $qb->orderBy('date', 'DESC')->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update attendance record.
|
||||
*
|
||||
* @param int $attendanceId
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function updateAttendance($attendanceId, $data)
|
||||
{
|
||||
return $this->update($attendanceId, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve attendance for a specific student, class section, and date.
|
||||
*
|
||||
* @param int $student_id
|
||||
* @param int $classSectionId
|
||||
* @param string $date
|
||||
* @return array|null
|
||||
*/
|
||||
public function getAttendance($student_id, $classSectionId, $date)
|
||||
{
|
||||
return $this->where([
|
||||
'student_id' => $student_id,
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $date
|
||||
])->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve reported attendance for a specific date or range.
|
||||
*
|
||||
* @param string $startDate
|
||||
* @param string|null $endDate
|
||||
* @return array
|
||||
*/
|
||||
public function getReportedAttendance($startDate, $endDate = null)
|
||||
{
|
||||
$builder = $this->where(['is_reported' => 1]);
|
||||
|
||||
if ($endDate) {
|
||||
$builder->where('date >=', $startDate)->where('date <=', $endDate);
|
||||
} else {
|
||||
$builder->where('date', $startDate);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
|
||||
public function getTodayAbsentees($classSectionId = null, $schoolYear = null, $semester = null)
|
||||
{
|
||||
$builder = $this->where('status', 'Absent')
|
||||
->where('date', date('Y-m-d'));
|
||||
|
||||
if ($classSectionId !== null) {
|
||||
$builder->where('class_section_id', $classSectionId);
|
||||
}
|
||||
|
||||
if ($schoolYear !== null) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if ($semester !== null) {
|
||||
$builder->where('semester', $semester);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
|
||||
|
||||
public function getTodayLates($classSectionId = null, $schoolYear = null, $semester = null)
|
||||
{
|
||||
$builder = $this->where('status', 'Late')
|
||||
->where('date', date('Y-m-d'));
|
||||
|
||||
if ($classSectionId !== null) {
|
||||
$builder->where('class_section_id', $classSectionId);
|
||||
}
|
||||
|
||||
if ($schoolYear !== null) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if ($semester !== null) {
|
||||
$builder->where('semester', $semester);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
|
||||
public function getAbsencesByClassSection($classSectionId, $schoolYear, $semester)
|
||||
{
|
||||
return $this->where('status', 'absent')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->orderBy('date', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch attendance rows for a student, optionally filtered by date range.
|
||||
*
|
||||
* @param int $studentId
|
||||
* @param string|null $startDate 'YYYY-MM-DD' inclusive
|
||||
* @param string|null $endDate 'YYYY-MM-DD' inclusive
|
||||
* @param string|null $schoolYear optional filter
|
||||
* @param string|null $semester optional filter
|
||||
* @return array
|
||||
*/
|
||||
public function getStudentAttendance(
|
||||
int $studentId,
|
||||
?string $startDate = null,
|
||||
?string $endDate = null,
|
||||
?string $schoolYear = null,
|
||||
?string $semester = null
|
||||
): array {
|
||||
$qb = $this->asArray()->where('student_id', $studentId);
|
||||
|
||||
if (!empty($startDate)) {
|
||||
$qb->where('date >=', $startDate);
|
||||
}
|
||||
if (!empty($endDate)) {
|
||||
$qb->where('date <=', $endDate);
|
||||
}
|
||||
if (!empty($schoolYear)) {
|
||||
$qb->where('school_year', $schoolYear);
|
||||
}
|
||||
if (!empty($semester)) {
|
||||
$qb->where('semester', $semester);
|
||||
}
|
||||
|
||||
return $qb->orderBy('date', 'DESC')->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: attendance for an exact date.
|
||||
*/
|
||||
public function getStudentAttendanceOnDate(int $studentId, string $date): ?array
|
||||
{
|
||||
return $this->asArray()
|
||||
->where(['student_id' => $studentId, 'date' => $date])
|
||||
->first(); // returns null if none
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT-REPORTED absences/lates for ALL students in optional date/term filters.
|
||||
* - Not reported: is_reported = 0 OR '0' OR NULL
|
||||
* - Status: ABS, ABSENT, LATE, A, L, plus prefixes like ABS_3 / LATE_2
|
||||
* - Date inclusive: >= start AND < (end + 1 day) to handle DATE/DATETIME columns
|
||||
*/
|
||||
public function getUnreportedAbsencesAndLates(
|
||||
?string $schoolYear = null,
|
||||
?string $semester = null
|
||||
): array {
|
||||
$b = $this->db->table($this->table);
|
||||
|
||||
$b->select('id, student_id, class_id, class_section_id, date, status, reason, school_year, semester, is_reported')
|
||||
// Not reported: 0 / '0' / NULL (supports legacy "reported" column)
|
||||
;
|
||||
$this->applyUnreportedFilter($b);
|
||||
// Not notified yet: exclude rows already marked notified (handles enum/string/numeric)
|
||||
$b->groupStart()
|
||||
->where('is_notified', 0)
|
||||
->orWhere('is_notified', '0')
|
||||
->orWhere('is_notified', null)
|
||||
->orWhere("LOWER(TRIM(COALESCE(is_notified, ''))) !=", 'yes')
|
||||
->groupEnd();
|
||||
|
||||
// Optional term filters (no date constraints)
|
||||
if ($schoolYear !== null && $schoolYear !== '') {
|
||||
$b->where("{$this->table}.school_year", $schoolYear);
|
||||
}
|
||||
if ($semester !== null && $semester !== '') {
|
||||
$b->where("{$this->table}.semester", $semester);
|
||||
}
|
||||
|
||||
// Status filter: exact + prefixes (ABS_3, LATE_2, etc.)
|
||||
$b->groupStart()
|
||||
->whereIn("{$this->table}.status", ['ABS','ABSENT','LATE','A','L'])
|
||||
->orLike("{$this->table}.status", 'ABS', 'after')
|
||||
->orLike("{$this->table}.status", 'LATE', 'after')
|
||||
->groupEnd();
|
||||
|
||||
// Sort for readability
|
||||
$b->orderBy("{$this->table}.student_id", 'ASC')
|
||||
->orderBy("{$this->table}.date", 'DESC');
|
||||
|
||||
$rows = $b->get()->getResultArray();
|
||||
|
||||
// Group per student (dates included in each record)
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$sid = (int) $r['student_id'];
|
||||
if (!isset($out[$sid])) {
|
||||
$out[$sid] = [
|
||||
'absences' => [],
|
||||
'lates' => [],
|
||||
'counts' => ['absences' => 0, 'lates' => 0, 'total' => 0],
|
||||
];
|
||||
}
|
||||
|
||||
$st = strtoupper((string)($r['status'] ?? ''));
|
||||
$isAbs = ($st === 'ABS' || $st === 'ABSENT' || $st === 'A' || str_starts_with($st, 'ABS'));
|
||||
$isLate = ($st === 'LATE' || $st === 'L' || str_starts_with($st, 'LATE'));
|
||||
|
||||
$rec = [
|
||||
'id' => $r['id'],
|
||||
'date' => $r['date'],
|
||||
'status' => $r['status'],
|
||||
'reason' => $r['reason'],
|
||||
'class_id' => $r['class_id'],
|
||||
'class_section_id' => $r['class_section_id'],
|
||||
'school_year' => $r['school_year'],
|
||||
'semester' => $r['semester'],
|
||||
];
|
||||
|
||||
if ($isAbs) {
|
||||
$out[$sid]['absences'][] = $rec;
|
||||
$out[$sid]['counts']['absences']++;
|
||||
} elseif ($isLate) {
|
||||
$out[$sid]['lates'][] = $rec;
|
||||
$out[$sid]['counts']['lates']++;
|
||||
}
|
||||
|
||||
$out[$sid]['counts']['total'] =
|
||||
$out[$sid]['counts']['absences'] + $out[$sid]['counts']['lates'];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function markRuleAsNotifiedData(
|
||||
int $studentId,
|
||||
string $date,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
$day = substr($date, 0, 10);
|
||||
$start = $day . ' 00:00:00';
|
||||
$endEx = date('Y-m-d', strtotime($day . ' +1 day')) . ' 00:00:00';
|
||||
$builder = $this->db->table($this->table);
|
||||
|
||||
$builder->where('student_id', $studentId)
|
||||
->where('date >=', $start)
|
||||
->where('date <', $endEx);
|
||||
|
||||
if (!empty($semester)) {
|
||||
$builder->where('semester', $semester);
|
||||
}
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
// ✅ Update ENUM field properly (compatible with legacy numeric flags)
|
||||
$builder->set('is_notified', 'yes');
|
||||
$builder->set('updated_at', utc_now());
|
||||
|
||||
return $builder->update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark specific attendance dates as reported + notified for a student.
|
||||
*
|
||||
* @param int $studentId
|
||||
* @param array $dates Array of YYYY-MM-DD strings
|
||||
*/
|
||||
public function markReportedAndNotified(int $studentId, array $dates, ?string $semester = null, ?string $schoolYear = null): bool
|
||||
{
|
||||
$dates = array_values(array_unique(array_filter($dates, static function ($d) {
|
||||
return is_string($d) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
|
||||
})));
|
||||
if (empty($dates)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$builder = $this->db->table($this->table)
|
||||
->where('student_id', $studentId)
|
||||
->whereIn('date', $dates)
|
||||
->where("LOWER(TRIM(status)) IN ('absent','late')", null, false);
|
||||
|
||||
if (!empty($semester)) {
|
||||
$builder->where('semester', $semester);
|
||||
}
|
||||
if (!empty($schoolYear)) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$cols = $this->reportedColumns();
|
||||
if (!empty($cols['is_reported'])) {
|
||||
$builder->set('is_reported', 'yes');
|
||||
}
|
||||
if (!empty($cols['reported'])) {
|
||||
$builder->set('reported', 1);
|
||||
}
|
||||
if ($this->db->fieldExists('is_notified', $this->table)) {
|
||||
$builder->set('is_notified', 'yes');
|
||||
}
|
||||
return (bool) $builder
|
||||
->set('updated_at', utc_now())
|
||||
->update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch unreported dates for a student within N days for the given status set.
|
||||
*
|
||||
* @param int $studentId
|
||||
* @param string[] $statuses ['absent','late']
|
||||
* @param string $incidentDate YYYY-MM-DD reference (defaults today)
|
||||
* @param int $lookbackDays days window backward from incidentDate (inclusive)
|
||||
*/
|
||||
public function getRecentUnreportedDates(
|
||||
int $studentId,
|
||||
array $statuses,
|
||||
?string $incidentDate = null,
|
||||
int $lookbackDays = 35,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
if ($studentId <= 0 || empty($statuses)) {
|
||||
return [];
|
||||
}
|
||||
$statuses = array_map('strtolower', $statuses);
|
||||
$day = $incidentDate && preg_match('/^\d{4}-\d{2}-\d{2}$/', $incidentDate)
|
||||
? $incidentDate
|
||||
: date('Y-m-d');
|
||||
$start = date('Y-m-d', strtotime($day . ' -' . max(0, $lookbackDays) . ' days'));
|
||||
|
||||
$qb = $this->db->table($this->table)
|
||||
->select('date')
|
||||
->where('student_id', $studentId)
|
||||
->where('date >=', $start)
|
||||
->where('date <=', $day)
|
||||
->whereIn('LOWER(TRIM(status))', $statuses);
|
||||
$this->applyUnreportedFilter($qb);
|
||||
|
||||
if (!empty($semester)) {
|
||||
$qb->where('semester', $semester);
|
||||
}
|
||||
if (!empty($schoolYear)) {
|
||||
$qb->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$rows = $qb->orderBy('date', 'DESC')->get()->getResultArray();
|
||||
return array_values(array_unique(array_map(static function ($r) {
|
||||
return substr((string)($r['date'] ?? ''), 0, 10);
|
||||
}, $rows)));
|
||||
}
|
||||
|
||||
private function reportedColumns(): array
|
||||
{
|
||||
if ($this->reportedColumns !== null) {
|
||||
return $this->reportedColumns;
|
||||
}
|
||||
$this->reportedColumns = [
|
||||
'is_reported' => $this->db->fieldExists('is_reported', $this->table),
|
||||
'reported' => $this->db->fieldExists('reported', $this->table),
|
||||
];
|
||||
return $this->reportedColumns;
|
||||
}
|
||||
|
||||
private function applyUnreportedFilter($qb): void
|
||||
{
|
||||
$cols = $this->reportedColumns();
|
||||
if (!empty($cols['is_reported'])) {
|
||||
$qb->groupStart()
|
||||
->where('is_reported', 'no')
|
||||
->orWhere('is_reported', 0)
|
||||
->orWhere('is_reported', '0')
|
||||
->orWhere('is_reported', null)
|
||||
->groupEnd();
|
||||
}
|
||||
if (!empty($cols['reported'])) {
|
||||
$qb->where("(LOWER(TRIM(COALESCE(reported, ''))) NOT IN ('yes','1','true','y','on'))", null, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class AttendanceDayModel extends Model
|
||||
{
|
||||
protected $table = 'attendance_day';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = false;
|
||||
|
||||
protected $allowedFields = [
|
||||
'class_section_id',
|
||||
'date',
|
||||
'status', // 'draft','submitted','published' (legacy: 'finalized')
|
||||
'submitted_by',
|
||||
'submitted_at',
|
||||
'published_by',
|
||||
'published_at',
|
||||
'auto_publish_at',
|
||||
'reopened_by',
|
||||
'reopened_at',
|
||||
'reopen_reason',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* Legacy helper kept for compatibility.
|
||||
* True iff this (section, date, term) is hard-locked for everyone.
|
||||
* Treats 'published' (new) and 'finalized' (old) as finalized.
|
||||
*/
|
||||
public function isFinalized(int $classSectionId, string $date, ?string $semester = null, ?string $schoolYear = null): bool
|
||||
{
|
||||
$builder = $this->builder()
|
||||
->select('id')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('date', $date)
|
||||
->groupStart()
|
||||
->where('status', 'published')
|
||||
->orWhere('status', 'finalized') // tolerate legacy data
|
||||
->groupEnd();
|
||||
|
||||
if ($semester !== null) $builder->where('semester', $semester);
|
||||
if ($schoolYear !== null) $builder->where('school_year', $schoolYear);
|
||||
|
||||
return (bool) $builder->get()->getFirstRow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a day row by (section, date, term). Returns array|null.
|
||||
*/
|
||||
public function findBySectionDate(int $classSectionId, string $date, ?string $semester, ?string $schoolYear): ?array
|
||||
{
|
||||
return $this->where([
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $date,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
])->first() ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the draft row for (section,date,term), creating it if missing.
|
||||
* Relies on the unique key (class_section_id, date, semester, school_year) to prevent duplicates.
|
||||
*/
|
||||
public function getOrCreateDraft(
|
||||
int $classSectionId,
|
||||
string $date,
|
||||
?string $semester,
|
||||
?string $schoolYear,
|
||||
int $userId = 0
|
||||
): array {
|
||||
// Try existing
|
||||
$row = $this->findBySectionDate($classSectionId, $date, $semester, $schoolYear);
|
||||
if ($row) return $row;
|
||||
|
||||
$now = utc_now();
|
||||
$data = [
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $date,
|
||||
'status' => 'draft',
|
||||
'submitted_by' => null,
|
||||
'submitted_at' => null,
|
||||
'published_by' => null,
|
||||
'published_at' => null,
|
||||
'auto_publish_at' => null,
|
||||
'reopened_by' => null,
|
||||
'reopened_at' => null,
|
||||
'reopen_reason' => null,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
try {
|
||||
$id = $this->insert($data, true);
|
||||
return $this->find($id);
|
||||
} catch (\Throwable $e) {
|
||||
// Race: someone else inserted the same unique key. Fetch it.
|
||||
$row = $this->findBySectionDate($classSectionId, $date, $semester, $schoolYear);
|
||||
if ($row) return $row;
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DEPRECATED: Prior code used "finalize" to lock immediately.
|
||||
* In the new workflow, "finalize" means "teacher submit"
|
||||
* (locks teachers, keeps admins editable). Use submit() or publish().
|
||||
*/
|
||||
public function finalize(int $id, int $userId): bool
|
||||
{
|
||||
// Alias to submit() to avoid hard-locking admins by legacy calls.
|
||||
return $this->submit($id, $userId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Teacher submit: status => submitted (teacher locked, admin editable).
|
||||
* Optionally set auto_publish_at (controller can compute second-Sunday rule).
|
||||
*/
|
||||
public function submit(int $id, int $userId, ?string $autoPublishAt = null): bool
|
||||
{
|
||||
$now = utc_now();
|
||||
return (bool) $this->update($id, [
|
||||
'status' => 'submitted',
|
||||
'submitted_by' => $userId,
|
||||
'submitted_at' => $now,
|
||||
'auto_publish_at' => $autoPublishAt,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin publish (hard lock): status => published.
|
||||
*/
|
||||
public function publish(int $id, int $userId): bool
|
||||
{
|
||||
$now = utc_now();
|
||||
return (bool) $this->update($id, [
|
||||
'status' => 'published',
|
||||
'published_by' => $userId,
|
||||
'published_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin reopen a submitted/published day back to 'draft' (default) or 'submitted'.
|
||||
* Provide an optional reason for audit.
|
||||
*/
|
||||
public function reopen(int $id, int $userId, string $toStatus = 'draft', ?string $reason = null): bool
|
||||
{
|
||||
if (!in_array($toStatus, ['draft', 'submitted'], true)) {
|
||||
throw new \InvalidArgumentException('toStatus must be "draft" or "submitted".');
|
||||
}
|
||||
|
||||
$now = utc_now();
|
||||
return (bool) $this->update($id, [
|
||||
'status' => $toStatus,
|
||||
'reopened_by' => $userId,
|
||||
'reopened_at' => $now,
|
||||
'reopen_reason' => $reason,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class AttendanceEmailTemplateModel extends Model
|
||||
{
|
||||
protected $table = 'email_templates';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $allowedFields = [
|
||||
'code', 'variant', 'subject', 'body_html', 'is_active', 'updated_by', 'updated_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* Fetch a template by code and variant, falling back to 'default' if needed.
|
||||
*/
|
||||
public function getTemplate(string $code, string $variant = 'default'): ?array
|
||||
{
|
||||
// Try exact (active only)
|
||||
$row = $this->where('code', $code)
|
||||
->where('variant', $variant)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
if ($row) {
|
||||
return $row;
|
||||
}
|
||||
|
||||
// Fallback to default variant
|
||||
$row = $this->where('code', $code)
|
||||
->where('variant', 'default')
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class AttendanceRecordModel extends Model
|
||||
{
|
||||
protected $table = 'attendance_record';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'class_section_id',
|
||||
'student_id',
|
||||
'school_id',
|
||||
'total_absence',
|
||||
'total_late',
|
||||
'total_presence',
|
||||
'total_attendance',
|
||||
'semester',
|
||||
'school_year',
|
||||
'modified_by',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
/**
|
||||
* Get attendance records by class section, semester, and school year.
|
||||
*
|
||||
* @param int $classSectionId
|
||||
* @param string $semester
|
||||
* @param string $school_year
|
||||
* @return array
|
||||
*/
|
||||
public function getAttendanceRecordByClass($classSectionId, $semester, $school_year)
|
||||
{
|
||||
return $this->where([
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $school_year
|
||||
])->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attendance record for a specific student in a class section.
|
||||
*
|
||||
* @param int $student_id
|
||||
* @param int $classSectionId
|
||||
* @param string $semester
|
||||
* @param string $school_year
|
||||
* @return array|null
|
||||
*/
|
||||
public function getAttendanceRecord($student_id, $classSectionId, $semester, $school_year)
|
||||
{
|
||||
return $this->where([
|
||||
'student_id' => $student_id,
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $school_year
|
||||
])->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing attendance record.
|
||||
*
|
||||
* @param int $attendanceId
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function updateAttendanceRecord($attendanceId, $data)
|
||||
{
|
||||
return $this->update($attendanceId, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment attendance counters for a student.
|
||||
*
|
||||
* @param int $student_id
|
||||
* @param int $classSectionId
|
||||
* @param string $semester
|
||||
* @param string $school_year
|
||||
* @param array $increments (e.g., ['total_absence' => 1, 'total_presence' => 0])
|
||||
* @return bool
|
||||
*/
|
||||
public function incrementAttendanceCounters($student_id, $classSectionId, $semester, $school_year, $increments)
|
||||
{
|
||||
$record = $this->getAttendanceRecord($student_id, $classSectionId, $semester, $school_year);
|
||||
|
||||
if ($record) {
|
||||
foreach ($increments as $field => $value) {
|
||||
if (isset($record[$field])) {
|
||||
$record[$field] += $value;
|
||||
}
|
||||
}
|
||||
return $this->update($record['id'], $record);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getTotalAbsences($studentId, $semester, $schoolYear)
|
||||
{
|
||||
$records = $this->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
|
||||
if (empty($records)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$totalAbsences = 0;
|
||||
foreach ($records as $record) {
|
||||
$totalAbsences += (int) ($record['total_absence'] ?? 0);
|
||||
}
|
||||
|
||||
return $totalAbsences;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class AttendanceTrackingModel extends Model
|
||||
{
|
||||
protected $table = 'attendance_tracking';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'date',
|
||||
'is_reported',
|
||||
'reason',
|
||||
'is_notified',
|
||||
'notif_counter',
|
||||
'semester',
|
||||
'school_year',
|
||||
'note',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected $dateFormat = 'datetime';
|
||||
|
||||
/* ----------------------- Helpers ----------------------- */
|
||||
|
||||
/** Normalize Y-m-d (or Y-m-d H:i:s) into a full 'Y-m-d H:i:s' at midnight if time missing. */
|
||||
private function normalizeDateTime(string $date): string
|
||||
{
|
||||
// If already has time, trust it; else append midnight
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $date)) {
|
||||
return $date;
|
||||
}
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
return $date . ' 00:00:00';
|
||||
}
|
||||
// Best-effort parse
|
||||
try {
|
||||
$t = Time::parse($date, 'UTC');
|
||||
} catch (\Throwable $e) {
|
||||
$t = Time::today('UTC');
|
||||
}
|
||||
return $t->toDateTimeString(); // Y-m-d H:i:s
|
||||
}
|
||||
|
||||
/** Apply semester/schoolYear filters if provided. */
|
||||
private function applyTermFilters($qb, ?string $semester, ?string $schoolYear)
|
||||
{
|
||||
if ($semester !== null && $semester !== '') {
|
||||
$qb->where('semester', $semester);
|
||||
}
|
||||
if ($schoolYear !== null && $schoolYear !== '') {
|
||||
$qb->where('school_year', $schoolYear);
|
||||
}
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/** Derive (semester, school_year) from a Y-m-d date (Fall=Aug–Dec; Spring=Jan–Jul). */
|
||||
private function deriveTermFromDate(string $ymd): array
|
||||
{
|
||||
try {
|
||||
$d = Time::createFromFormat('Y-m-d', substr($ymd, 0, 10), 'UTC') ?? Time::today('UTC');
|
||||
} catch (\Throwable $e) {
|
||||
$d = Time::today('UTC');
|
||||
}
|
||||
$m = (int)$d->format('n');
|
||||
$y = (int)$d->format('Y');
|
||||
|
||||
$semester = ($m >= 8 && $m <= 12) ? 'Fall' : 'Spring';
|
||||
$startY = ($m >= 8) ? $y : $y - 1;
|
||||
$endY = $startY + 1;
|
||||
|
||||
return [$semester, sprintf('%d-%d', $startY, $endY)];
|
||||
}
|
||||
|
||||
/* ----------------------- CRUD-ish APIs ----------------------- */
|
||||
|
||||
/**
|
||||
* Records a student absence (stores time at 00:00:00 if only a day is given).
|
||||
* NOTE: $semester/$schoolYear are optional; will be derived from $date if omitted.
|
||||
*/
|
||||
public function recordAbsence(
|
||||
int $studentId,
|
||||
string $date,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null,
|
||||
bool $isExcused = false,
|
||||
?string $reason = null
|
||||
) {
|
||||
$dt = $this->normalizeDateTime($date);
|
||||
|
||||
if ($semester === null || $schoolYear === null) {
|
||||
[$sem, $yr] = $this->deriveTermFromDate($dt);
|
||||
$semester = $semester ?? $sem;
|
||||
$schoolYear = $schoolYear ?? $yr;
|
||||
}
|
||||
|
||||
return $this->insert([
|
||||
'student_id' => $studentId,
|
||||
'date' => $dt,
|
||||
'is_reported' => $isExcused ? 1 : 0,
|
||||
'reason' => $reason,
|
||||
'is_notified' => 0,
|
||||
'notif_counter' => 0,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* All absences for a student; start/end accept Y-m-d or Y-m-d H:i:s.
|
||||
* For day-only ranges, we compare on DATE(date).
|
||||
*/
|
||||
public function getStudentAbsences(
|
||||
int $studentId,
|
||||
?string $startDate = null,
|
||||
?string $endDate = null,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
$qb = $this->builder()->where('student_id', $studentId);
|
||||
|
||||
if ($startDate && preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
|
||||
$qb->where('DATE(`date`) >=', $startDate);
|
||||
} elseif ($startDate) {
|
||||
$qb->where('date >=', $this->normalizeDateTime($startDate));
|
||||
}
|
||||
|
||||
if ($endDate && preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
|
||||
$qb->where('DATE(`date`) <=', $endDate);
|
||||
} elseif ($endDate) {
|
||||
$qb->where('date <=', $this->normalizeDateTime($endDate));
|
||||
}
|
||||
|
||||
$this->applyTermFilters($qb, $semester, $schoolYear);
|
||||
|
||||
return $qb->orderBy('date', 'DESC')->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unexcused + unnotified absences (optionally for a specific day).
|
||||
*/
|
||||
public function getUnnotifiedUnexcusedAbsences(
|
||||
?string $date = null,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
$qb = $this->builder()
|
||||
->where('is_reported', 0)
|
||||
->where('is_notified', 0);
|
||||
|
||||
if ($date && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
$qb->where('DATE(`date`)', $date);
|
||||
} elseif ($date) {
|
||||
$qb->where('date', $this->normalizeDateTime($date));
|
||||
}
|
||||
|
||||
$this->applyTermFilters($qb, $semester, $schoolYear);
|
||||
|
||||
return $qb->orderBy('date', 'DESC')->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Has at least N absences in the last N days (rough heuristic);
|
||||
* for weekly checks, prefer checkConsecutiveAbsences().
|
||||
*/
|
||||
public function hasConsecutiveAbsences(
|
||||
int $studentId,
|
||||
int $days,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
$start = date('Y-m-d', strtotime("-{$days} days"));
|
||||
|
||||
$qb = $this->builder()
|
||||
->select('COUNT(*) AS cnt')
|
||||
->where('student_id', $studentId)
|
||||
->where('DATE(`date`) >=', $start);
|
||||
|
||||
$this->applyTermFilters($qb, $semester, $schoolYear);
|
||||
|
||||
$row = $qb->get()->getFirstRow('array');
|
||||
return (int)($row['cnt'] ?? 0) >= $days;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return TRUE if last $threshold absences are exactly weekly (7-day spacing).
|
||||
*/
|
||||
public function checkConsecutiveAbsences(
|
||||
int $studentId,
|
||||
int $threshold,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
$qb = $this->builder()
|
||||
->select('date, is_reported, is_notified')
|
||||
->where('student_id', $studentId)
|
||||
->where('is_reported', 0)
|
||||
->where('is_notified', 0);
|
||||
|
||||
$this->applyTermFilters($qb, $semester, $schoolYear);
|
||||
|
||||
$absences = $qb->orderBy('date', 'DESC')->limit($threshold)->get()->getResultArray();
|
||||
|
||||
if (count($absences) < $threshold) return false;
|
||||
|
||||
for ($i = 0; $i < $threshold - 1; $i++) {
|
||||
$d1 = new \DateTime($absences[$i]['date']);
|
||||
$d2 = new \DateTime($absences[$i + 1]['date']);
|
||||
$diffDays = (int)$d2->diff($d1)->format('%a');
|
||||
if ($diffDays !== 7) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* TRUE if at least $absencesThreshold absences in the last $weeksStreak weeks.
|
||||
*/
|
||||
public function checkAbsencesInStreak(
|
||||
int $studentId,
|
||||
int $absencesThreshold,
|
||||
int $weeksStreak,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
$qb = $this->builder()
|
||||
->select('date, is_reported, is_notified')
|
||||
->where('student_id', $studentId)
|
||||
->where('is_reported', 0)
|
||||
->where('is_notified', 0);
|
||||
|
||||
$this->applyTermFilters($qb, $semester, $schoolYear);
|
||||
|
||||
$records = $qb->orderBy('date', 'DESC')->limit($weeksStreak)->get()->getResultArray();
|
||||
|
||||
if (count($records) < $weeksStreak) return false;
|
||||
|
||||
$count = 0;
|
||||
foreach ($records as $r) {
|
||||
if ((int)$r['is_reported'] === 0 && (int)$r['is_notified'] === 0) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
return $count >= $absencesThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize a student's status this term.
|
||||
*/
|
||||
public function getStudentAttendanceStats(
|
||||
int $studentId,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
// total_absences
|
||||
$qb1 = $this->builder()->where('student_id', $studentId);
|
||||
$this->applyTermFilters($qb1, $semester, $schoolYear);
|
||||
$total = (int)($qb1->select('COUNT(*) AS cnt')->get()->getFirstRow('array')['cnt'] ?? 0);
|
||||
|
||||
// unexcused_absences
|
||||
$qb2 = $this->builder()->where('student_id', $studentId)->where('is_reported', 0);
|
||||
$this->applyTermFilters($qb2, $semester, $schoolYear);
|
||||
$unexcused = (int)($qb2->select('COUNT(*) AS cnt')->get()->getFirstRow('array')['cnt'] ?? 0);
|
||||
|
||||
// last_absence
|
||||
$qb3 = $this->builder()->where('student_id', $studentId);
|
||||
$this->applyTermFilters($qb3, $semester, $schoolYear);
|
||||
$last = $qb3->orderBy('date', 'DESC')->get()->getFirstRow('array');
|
||||
|
||||
return [
|
||||
'total_absences' => $total,
|
||||
'unexcused_absences' => $unexcused,
|
||||
'last_absence' => $last,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch records by semester/schoolYear (with optional limit).
|
||||
*/
|
||||
public function getAttendanceBySemester(
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null,
|
||||
int $limit = 0
|
||||
): array {
|
||||
$qb = $this->builder();
|
||||
$this->applyTermFilters($qb, $semester, $schoolYear);
|
||||
if ($limit > 0) $qb->limit($limit);
|
||||
return $qb->orderBy('date', 'DESC')->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary counts for a term.
|
||||
*/
|
||||
public function getSemesterSummary(?string $semester = null, ?string $schoolYear = null): array
|
||||
{
|
||||
// total
|
||||
$qb1 = $this->builder();
|
||||
$this->applyTermFilters($qb1, $semester, $schoolYear);
|
||||
$total = (int)($qb1->select('COUNT(*) AS cnt')->get()->getFirstRow('array')['cnt'] ?? 0);
|
||||
|
||||
// unexcused
|
||||
$qb2 = $this->builder()->where('is_reported', 0);
|
||||
$this->applyTermFilters($qb2, $semester, $schoolYear);
|
||||
$unexcused = (int)($qb2->select('COUNT(*) AS cnt')->get()->getFirstRow('array')['cnt'] ?? 0);
|
||||
|
||||
// distinct students
|
||||
$qb3 = $this->builder();
|
||||
$this->applyTermFilters($qb3, $semester, $schoolYear);
|
||||
$students = (int)($qb3->select('COUNT(DISTINCT student_id) AS cnt')->get()->getFirstRow('array')['cnt'] ?? 0);
|
||||
|
||||
return [
|
||||
'total_absences' => $total,
|
||||
'unexcused_absences' => $unexcused,
|
||||
'students_with_absences' => $students,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update/create violation rows (matching by student + DATE(date) + term).
|
||||
* Expects input rows like: ['id'=>..., 'violation'=>..., 'absences'=>[['date'=>'Y-m-d'], ...]]
|
||||
*/
|
||||
public function updateAttendanceViolations(array $studentsWithViolations, string $semester, string $schoolYear): array
|
||||
{
|
||||
$results = ['added' => 0, 'updated' => 0, 'skipped' => 0, 'notified' => 0];
|
||||
|
||||
foreach ($studentsWithViolations as $student) {
|
||||
if (empty($student['absences'])) {
|
||||
$results['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($student['absences'] as $absence) {
|
||||
$ymd = substr((string)($absence['date'] ?? date('Y-m-d')), 0, 10);
|
||||
$dt = $this->normalizeDateTime($ymd);
|
||||
|
||||
$data = [
|
||||
'student_id' => (int)$student['id'],
|
||||
'date' => $dt,
|
||||
'is_reported' => 0,
|
||||
'reason' => (string)($student['violation'] ?? ''),
|
||||
'is_notified' => 0,
|
||||
'notif_counter' => 0,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
];
|
||||
|
||||
try {
|
||||
// Match by student + DATE(date) + term
|
||||
$existing = $this->builder()
|
||||
->where('student_id', $data['student_id'])
|
||||
->where('DATE(`date`)', $ymd)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()->getFirstRow('array');
|
||||
|
||||
if ($existing) {
|
||||
if (($existing['reason'] ?? '') !== $data['reason']) {
|
||||
$this->update((int)$existing['id'], $data);
|
||||
$results['updated']++;
|
||||
|
||||
if (!empty($existing['is_notified'])) {
|
||||
$this->resetNotification((int)$existing['id']);
|
||||
$results['notified']++;
|
||||
}
|
||||
} else {
|
||||
$results['skipped']++;
|
||||
}
|
||||
} else {
|
||||
$newId = $this->insert($data, true);
|
||||
$results['added']++;
|
||||
|
||||
// Only match a standalone "4" (final threshold)
|
||||
if (preg_match('/\b4\b/', $data['reason'])) {
|
||||
$this->markAsNotified((int)$newId);
|
||||
$results['notified']++;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', sprintf(
|
||||
'Failed to update attendance violation for student %d on %s: %s',
|
||||
$data['student_id'],
|
||||
$ymd,
|
||||
$e->getMessage()
|
||||
));
|
||||
$results['skipped']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/* ----------------------- Notification ops ----------------------- */
|
||||
|
||||
public function markAsNotified(int $id): bool
|
||||
{
|
||||
// atomic increment + set flag; touch updated_at by doing update()
|
||||
$now = utc_now();
|
||||
$this->builder()
|
||||
->where('id', $id)
|
||||
->set('notif_counter', 'COALESCE(notif_counter,0)+1', false)
|
||||
->update();
|
||||
return $this->update($id, [
|
||||
'is_notified' => 1,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
public function resetNotification(int $id): bool
|
||||
{
|
||||
return $this->update($id, ['is_notified' => 0, 'notif_counter' => 0]);
|
||||
}
|
||||
|
||||
public function markRuleAsNotified(
|
||||
int $studentId,
|
||||
string $code,
|
||||
string $ymd,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
$semester = $semester ?? (new \App\Models\ConfigurationModel())->getConfig('semester');
|
||||
$schoolYear = $schoolYear ?? (new \App\Models\ConfigurationModel())->getConfig('school_year');
|
||||
|
||||
$day = substr($ymd, 0, 10);
|
||||
$start = $day . ' 00:00:00';
|
||||
$end = date('Y-m-d', strtotime($day . ' +1 day')) . ' 00:00:00';
|
||||
$now = utc_now();
|
||||
|
||||
// 1) Try match by code + day (preferred)
|
||||
$qb = $this->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date >=', $start)
|
||||
->where('date <', $end)
|
||||
->like('reason', $code, 'both')
|
||||
->orderBy('date', 'DESC');
|
||||
|
||||
$row = $qb->first();
|
||||
|
||||
// 2) Fallback: match by day only (reason may hold a human-friendly title)
|
||||
if (!$row) {
|
||||
$row = $this->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date >=', $start)
|
||||
->where('date <', $end)
|
||||
->orderBy('date', 'DESC')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($row && !empty($row['id'])) {
|
||||
$count = (int)($row['notif_counter'] ?? 0) + 1;
|
||||
return (bool) $this->update((int)$row['id'], [
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => $count,
|
||||
'is_reported' => 0,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
// No existing row -> insert one so future passes see it as notified
|
||||
return (bool) $this->insert([
|
||||
'student_id' => $studentId,
|
||||
'date' => $start,
|
||||
'is_reported' => 0,
|
||||
'reason' => $code, // store the CODE, not a prose title
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => 1,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the latest matching tracking row as notified and increment counter.
|
||||
* $code is the rule code you stored in "reason" (e.g. ABS_2, LATE_3, MIX_L2A1).
|
||||
* Optionally bound by $lastDate if you want to avoid touching future rows.
|
||||
*/
|
||||
public function markNotified(int $studentId, string $schoolYear, ?string $semester, string $code, ?string $lastDate = null): int
|
||||
{
|
||||
$qb = $this->builder();
|
||||
$qb->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if (!empty($semester)) {
|
||||
$qb->where('semester', $semester);
|
||||
}
|
||||
|
||||
if (!empty($lastDate)) {
|
||||
$qb->where('date <=', $lastDate); // keep it safe to the last violation date you emailed about
|
||||
}
|
||||
|
||||
$qb->like('reason', $code, 'both')
|
||||
->orderBy('date', 'DESC')
|
||||
->limit(1);
|
||||
|
||||
$row = $qb->get()->getRowArray();
|
||||
if (!$row) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$id = (int)$row['id'];
|
||||
$counter = (int)($row['notif_counter'] ?? 0) + 1;
|
||||
|
||||
return $this->update($id, [
|
||||
'is_notified' => 1,
|
||||
'notif_counter'=> $counter,
|
||||
'updated_at' => utc_now(),
|
||||
]) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class AuthorizedUserModel extends Model
|
||||
{
|
||||
protected $table = 'authorized_users'; // Table name
|
||||
protected $primaryKey = 'id'; // Primary key of the table
|
||||
|
||||
// Allowed fields for mass assignment
|
||||
protected $allowedFields = [
|
||||
'user_id',
|
||||
'authorized_user_id',
|
||||
'firstname',
|
||||
'lastname',
|
||||
'phone_number',
|
||||
'email',
|
||||
'relation_to_user',
|
||||
'token',
|
||||
'status',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $returnType = 'array'; // Return data as an array
|
||||
|
||||
// Automatically manage created_at and updated_at fields
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
// Validation rules
|
||||
protected $validationRules = [
|
||||
'email' => 'required|valid_email',
|
||||
'user_id' => 'required|integer',
|
||||
'firstname' => 'required|string|max_length[255]',
|
||||
'lastname' => 'required|string|max_length[255]',
|
||||
'phone_number' => 'required|string|max_length[25]',
|
||||
'relation_to_user' => 'string|max_length[64]',
|
||||
'status' => 'required|in_list[Pending,Active]',
|
||||
];
|
||||
|
||||
// Custom validation messages
|
||||
protected $validationMessages = [
|
||||
'email' => [
|
||||
'required' => 'Email is required',
|
||||
'valid_email' => 'Email must be valid',
|
||||
],
|
||||
'user_id' => [
|
||||
'required' => 'User ID is required',
|
||||
'integer' => 'User ID must be an integer',
|
||||
],
|
||||
'firstname' => [
|
||||
'required' => 'First name is required',
|
||||
'string' => 'First name must be a string',
|
||||
'max_length' => 'First name cannot exceed 255 characters',
|
||||
],
|
||||
'lastname' => [
|
||||
'required' => 'Last name is required',
|
||||
'string' => 'Last name must be a string',
|
||||
'max_length' => 'Last name cannot exceed 255 characters',
|
||||
],
|
||||
'phone_number' => [
|
||||
'required' => 'Phone number is required',
|
||||
'string' => 'Phone number must be a string',
|
||||
'max_length' => 'Phone number cannot exceed 25 characters',
|
||||
],
|
||||
'relation_to_user' => [
|
||||
'string' => 'Relation to user must be a string',
|
||||
'max_length' => 'Relation to user cannot exceed 64 characters',
|
||||
],
|
||||
'status' => [
|
||||
'required' => 'Status is required',
|
||||
'in_list' => 'Status must be either Pending or Active',
|
||||
],
|
||||
];
|
||||
|
||||
// Optional: Skip validation for bulk inserts
|
||||
protected $skipValidation = false;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class BadgePrintLogModel extends Model
|
||||
{
|
||||
protected $table = 'badge_print_logs';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useTimestamps = false;
|
||||
protected $returnType = 'array';
|
||||
protected $allowedFields = [
|
||||
'user_id',
|
||||
'printed_by',
|
||||
'school_year',
|
||||
'printed_at',
|
||||
'role',
|
||||
'class_section_name',
|
||||
'copies',
|
||||
];
|
||||
|
||||
/**
|
||||
* Insert a batch of print logs. Swallows errors if table is missing.
|
||||
*
|
||||
* @param int[] $userIds
|
||||
* @param int|null $printedBy
|
||||
* @param string|null $schoolYear
|
||||
* @param array $roleMap userId => role label
|
||||
* @param array $classMap userId => class name
|
||||
* @param int $copies copies per user (defaults to 1)
|
||||
* @return int rows inserted (best-effort)
|
||||
*/
|
||||
public function logPrints(array $userIds, ?int $printedBy, ?string $schoolYear, array $roleMap = [], array $classMap = [], int $copies = 1): int
|
||||
{
|
||||
$now = utc_now();
|
||||
$rows = [];
|
||||
|
||||
foreach ($userIds as $uid) {
|
||||
$uid = (int) $uid;
|
||||
if ($uid <= 0) continue;
|
||||
$rows[] = [
|
||||
'user_id' => $uid,
|
||||
'printed_by' => $printedBy,
|
||||
'school_year' => $schoolYear,
|
||||
'printed_at' => $now,
|
||||
'role' => (string)($roleMap[$uid] ?? ''),
|
||||
'class_section_name' => (string)($classMap[$uid] ?? ''),
|
||||
'copies' => max(1, (int)$copies),
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($rows)) return 0;
|
||||
|
||||
try {
|
||||
return $this->insertBatch($rows) ?? count($rows);
|
||||
} catch (\Throwable $e) {
|
||||
// If table does not exist or other DB error, avoid breaking PDF generation
|
||||
log_message('error', 'BadgePrintLogModel::logPrints failed: ' . $e->getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return counts and last printed info for given user IDs.
|
||||
*
|
||||
* @param int[] $userIds
|
||||
* @param string|null $schoolYear Optional filter by school year
|
||||
* @return array userId => [count, last_printed_at, last_printed_by]
|
||||
*/
|
||||
public function getStatus(array $userIds, ?string $schoolYear = null): array
|
||||
{
|
||||
$userIds = array_values(array_unique(array_map('intval', array_filter($userIds))));
|
||||
if (empty($userIds)) return [];
|
||||
|
||||
try {
|
||||
$builder = $this->db->table($this->table . ' l')
|
||||
->select('l.user_id, COUNT(*) AS prints, MAX(l.printed_at) AS last_printed_at, MAX(l.printed_by) AS last_printed_by')
|
||||
->whereIn('l.user_id', $userIds)
|
||||
->groupBy('l.user_id');
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$builder->where('l.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$rows = $builder->get()->getResultArray();
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'BadgePrintLogModel::getStatus failed: ' . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$out[(int)$r['user_id']] = [
|
||||
'count' => (int)($r['prints'] ?? 0),
|
||||
'last_printed_at' => (string)($r['last_printed_at'] ?? ''),
|
||||
'last_printed_by' => isset($r['last_printed_by']) ? (int)$r['last_printed_by'] : null,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class CalendarModel extends Model
|
||||
{
|
||||
protected $table = 'calendar_events';
|
||||
protected $primaryKey = 'id';
|
||||
protected ?bool $hasEventTypeColumn = null;
|
||||
|
||||
protected $allowedFields = [
|
||||
'title',
|
||||
'date',
|
||||
'description',
|
||||
'event_type',
|
||||
'semester',
|
||||
'school_year',
|
||||
'notify_parent',
|
||||
'notify_admin',
|
||||
'notify_teacher',
|
||||
'no_school'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
public function getEvents()
|
||||
{
|
||||
$events = $this->findAll();
|
||||
return $this->withNoSchoolFlag($events);
|
||||
}
|
||||
|
||||
public function getEventByDate($date)
|
||||
{
|
||||
$events = $this->where('date', $date)->findAll();
|
||||
return $this->withNoSchoolFlag($events);
|
||||
}
|
||||
|
||||
public function addEvent($data)
|
||||
{
|
||||
return $this->insert($data);
|
||||
}
|
||||
|
||||
public function getEventsBySchoolYear($schoolYear)
|
||||
{
|
||||
$events = $this->where('school_year', $schoolYear)
|
||||
->orderBy('date', 'ASC')
|
||||
->findAll();
|
||||
return $this->withNoSchoolFlag($events);
|
||||
}
|
||||
|
||||
public function getEventsBySchoolYearAndSemester(string $schoolYear, ?string $semester = null): array
|
||||
{
|
||||
$builder = $this->where('school_year', $schoolYear);
|
||||
if (is_string($semester) && $semester !== '') {
|
||||
$builder = $builder->where('semester', $semester);
|
||||
}
|
||||
$events = $builder->orderBy('date', 'ASC')->findAll();
|
||||
return $this->withNoSchoolFlag($events);
|
||||
}
|
||||
|
||||
public function supportsEventType(): bool
|
||||
{
|
||||
if ($this->hasEventTypeColumn === null) {
|
||||
$this->hasEventTypeColumn = $this->db->fieldExists('event_type', $this->table);
|
||||
}
|
||||
return $this->hasEventTypeColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add computed no_school flag to each event without requiring DB schema changes.
|
||||
* An event is considered no-school if title or description contains keywords.
|
||||
*
|
||||
* @param array $events
|
||||
* @return array
|
||||
*/
|
||||
protected function withNoSchoolFlag(array $events): array
|
||||
{
|
||||
return array_map(function ($event) {
|
||||
// Respect DB value if present; otherwise infer heuristically
|
||||
if (array_key_exists('no_school', (array) $event)) {
|
||||
$event['no_school'] = (int) ((array) $event)['no_school'];
|
||||
} else {
|
||||
$event['no_school'] = $this->isNoSchoolEvent((array) $event) ? 1 : 0;
|
||||
}
|
||||
return $event;
|
||||
}, $events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic to determine "no school" days from event text.
|
||||
*/
|
||||
protected function isNoSchoolEvent(array $event): bool
|
||||
{
|
||||
$title = strtolower((string)($event['title'] ?? ''));
|
||||
$desc = strtolower((string)($event['description'] ?? ''));
|
||||
$text = $title . ' ' . $desc;
|
||||
$keywords = ['no school', 'no-school', 'cancel', 'vacation', 'holiday', 'break'];
|
||||
foreach ($keywords as $kw) {
|
||||
if ($kw !== '' && strpos($text, $kw) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ClassModel extends Model
|
||||
{
|
||||
protected $table = 'classes'; // The table name
|
||||
protected $primaryKey = 'id'; // The correct primary key for the classes table
|
||||
|
||||
protected $allowedFields = [
|
||||
'class_name',
|
||||
'schedule',
|
||||
'capacity'
|
||||
]; // Fields that are allowed to be manipulated
|
||||
|
||||
protected $useTimestamps = false;
|
||||
|
||||
public function getAllClasses()
|
||||
{
|
||||
return $this->findAll();
|
||||
}
|
||||
|
||||
public function getClassById($id)
|
||||
{
|
||||
return $this->find($id);
|
||||
}
|
||||
|
||||
public function createClass($data)
|
||||
{
|
||||
return $this->insert($data);
|
||||
}
|
||||
|
||||
public function updateClass($id, $data)
|
||||
{
|
||||
return $this->update($id, $data);
|
||||
}
|
||||
|
||||
public function deleteClass($id)
|
||||
{
|
||||
return $this->delete($id);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ClassPrepAdjustmentModel extends Model
|
||||
{
|
||||
protected $table = 'class_prep_adjustments';
|
||||
protected $allowedFields = [
|
||||
'class_section_id', 'item_name', 'adjustment', 'school_year', 'created_at'
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ClassPreparationLogModel extends Model
|
||||
{
|
||||
protected $table = 'class_preparation_log';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'class_section_id',
|
||||
'class_section',
|
||||
'school_year',
|
||||
'prep_data',
|
||||
'created_at',
|
||||
];
|
||||
public $useTimestamps = false;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ClassProgressAttachmentModel extends Model
|
||||
{
|
||||
protected $table = 'class_progress_attachments';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $allowedFields = [
|
||||
'report_id',
|
||||
'file_path',
|
||||
'original_name',
|
||||
'mime_type',
|
||||
'file_size',
|
||||
'created_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ClassProgressReportModel extends Model
|
||||
{
|
||||
protected $table = 'class_progress_reports';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $allowedFields = [
|
||||
'class_section_id',
|
||||
'teacher_id',
|
||||
'week_start',
|
||||
'week_end',
|
||||
'subject',
|
||||
'unit_title',
|
||||
'covered',
|
||||
'homework',
|
||||
'assessment',
|
||||
'status',
|
||||
'status_notes',
|
||||
'class_notes',
|
||||
'next_week_plan',
|
||||
'support_needed',
|
||||
'flags_json',
|
||||
'attachment_path',
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ClassSectionModel extends Model
|
||||
{
|
||||
protected $table = 'classSection'; // Correct table name
|
||||
protected $primaryKey = 'id'; // Specify the primary key field
|
||||
|
||||
protected $allowedFields = [
|
||||
'class_id',
|
||||
'class_section_id',
|
||||
'class_section_name',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
// Set default return type
|
||||
protected $returnType = 'array';
|
||||
|
||||
// Method to get class sections with class and section names
|
||||
public function getClassSections(): array
|
||||
{
|
||||
return $this->select('classSection.class_section_id, classSection.class_section_name, classes.class_name')
|
||||
->join('classes', 'classSection.class_id = classes.id', 'left')
|
||||
->orderBy('classSection.class_section_name', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent class_id for a given class_section_id.
|
||||
*
|
||||
* @param int|string $classSectionId
|
||||
* @return int|null class_id if found, otherwise null
|
||||
*/
|
||||
public function getClassId($classSectionId): ?int
|
||||
{
|
||||
if ($classSectionId === null || $classSectionId === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $this->select('class_id')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->orderBy($this->primaryKey, 'DESC') // in case of duplicates, take latest
|
||||
->first();
|
||||
|
||||
return $row ? (int) $row['class_id'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class_section_name for a given section_id.
|
||||
*
|
||||
* @param int|string $sectionId
|
||||
* @return string|null
|
||||
*/
|
||||
public function getClassSectionNameBySectionId($sectionId)
|
||||
{
|
||||
$result = $this->where('class_section_id', $sectionId)->first();
|
||||
return $result['class_section_name'] ?? null;
|
||||
}
|
||||
|
||||
public function getClassSectionNameByClassId($classId)
|
||||
{
|
||||
$result = $this->where('class_id', $classId)->first();
|
||||
return $result['class_section_name'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get section ID from class section name.
|
||||
*
|
||||
* @param string $classSectionName The name of the class section.
|
||||
* @return mixed The section ID or null if no match is found.
|
||||
*/
|
||||
public function getSectionIDFromClassSectionName($classSectionName)
|
||||
{
|
||||
// Query the table to find the class section by its name
|
||||
$result = $this->where('class_section_name', $classSectionName)
|
||||
->first(); // Get the first result (assuming class section name is unique)
|
||||
|
||||
// Return the class section_id if the result is found, otherwise return null
|
||||
return $result ? $result['class_section_id'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the base (non-letter) section row for a given class_id. E.g., '3' vs '3-A'.
|
||||
*/
|
||||
public function getBaseSectionByClassId(int $classId): ?array
|
||||
{
|
||||
// Heuristic: names without a dash are base sections (e.g., '3', 'KG', 'youth')
|
||||
return $this->where('class_id', $classId)
|
||||
->where("class_section_name NOT LIKE '%-%'", null, false)
|
||||
->orderBy('id', 'ASC')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return lettered sections for a class (e.g., '3-A','3-B',...). Ordered by name asc.
|
||||
*/
|
||||
public function getLetterSectionsByClassId(int $classId): array
|
||||
{
|
||||
return $this->where('class_id', $classId)
|
||||
->like('class_section_name', '-', 'both')
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class CommunicationLogModel extends Model {
|
||||
protected $table = 'communication_logs';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'student_id','family_id','student_name','template_key','subject','body',
|
||||
'recipients','cc','bcc','attachments','status','error_message','sent_by','metadata'
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class CompetitionClassWinnerModel extends Model
|
||||
{
|
||||
protected $table = 'competition_class_winners';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'competition_id',
|
||||
'class_section_id',
|
||||
'winners_override',
|
||||
'question_count',
|
||||
'prize_1',
|
||||
'prize_2',
|
||||
'prize_3',
|
||||
'prize_4',
|
||||
'prize_5',
|
||||
'prize_6',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class CompetitionModel extends Model
|
||||
{
|
||||
protected $table = 'competitions';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'title',
|
||||
'semester',
|
||||
'school_year',
|
||||
'class_section_id',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'winners_count',
|
||||
'prize_per_point',
|
||||
'is_published',
|
||||
'published_at',
|
||||
'is_locked',
|
||||
'locked_at',
|
||||
'locked_by',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class CompetitionScoreModel extends Model
|
||||
{
|
||||
protected $table = 'competition_scores';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'competition_id',
|
||||
'student_id',
|
||||
'class_section_id',
|
||||
'score',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class CompetitionWinnerModel extends Model
|
||||
{
|
||||
protected $table = 'competition_winners';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'competition_id',
|
||||
'student_id',
|
||||
'class_section_id',
|
||||
'rank',
|
||||
'score',
|
||||
'prize_amount',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ConfigurationModel extends Model
|
||||
{
|
||||
protected $table = 'configuration'; // The name of the table
|
||||
protected $primaryKey = 'id'; // The primary key of the table
|
||||
|
||||
protected $allowedFields = [
|
||||
'config_key',
|
||||
'config_value',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get configuration value by key.
|
||||
*
|
||||
* @param string $key
|
||||
* @return string|null
|
||||
*/
|
||||
public function getConfigValueByKey(string $key)
|
||||
{
|
||||
// Deterministic read in case historical duplicates exist
|
||||
$result = $this->where('config_key', $key)
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
return $result ? $result['config_value'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration value by key.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @return bool
|
||||
*/
|
||||
public function setConfigValueByKey(string $key, string $value): bool
|
||||
{
|
||||
// If one or more rows exist for this key, update ALL of them to avoid
|
||||
// inconsistent reads when duplicates are present.
|
||||
$count = $this->where('config_key', $key)->countAllResults();
|
||||
|
||||
if ($count > 0) {
|
||||
// Use a direct builder update scoped by key to affect all matches
|
||||
$ok = (bool) $this->db->table($this->table)
|
||||
->where('config_key', $key)
|
||||
->update(['config_value' => $value]);
|
||||
return $ok;
|
||||
}
|
||||
|
||||
// Insert a new record if none exist
|
||||
return $this->insert(['config_key' => $key, 'config_value' => $value]) !== false;
|
||||
}
|
||||
|
||||
// Method to retrieve all configuration data
|
||||
public function getAllConfigs()
|
||||
{
|
||||
return $this->findAll();
|
||||
}
|
||||
|
||||
// Method to update configuration by key
|
||||
public function updateConfig($id, $data)
|
||||
{
|
||||
return $this->update($id, $data);
|
||||
}
|
||||
|
||||
// Method to add new configuration
|
||||
public function addConfig($data)
|
||||
{
|
||||
return $this->insert($data);
|
||||
}
|
||||
|
||||
public function getConfig($key)
|
||||
{
|
||||
$key = (string) $key;
|
||||
if ($key === 'semester') {
|
||||
try {
|
||||
$semester = (new \App\Services\SemesterRangeService($this))->getSemesterForDate();
|
||||
if ($semester !== '') {
|
||||
return $semester;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore and fall back
|
||||
}
|
||||
}
|
||||
|
||||
return $this->getConfigValueByKey($key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ContactUsModel extends Model
|
||||
{
|
||||
protected $table = 'contactus';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
protected $allowedFields = [
|
||||
'sender_id',
|
||||
'reciever_id',
|
||||
'subject',
|
||||
'message',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
// Timestamps (set manually or use auto timestamps below)
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
protected $validationRules = [
|
||||
'sender_id' => 'required|integer',
|
||||
'reciever_id' => 'required|integer',
|
||||
'subject' => 'required|string|max_length[255]',
|
||||
'message' => 'required|string',
|
||||
'semester' => 'required|string|max_length[255]',
|
||||
'school_year' => 'permit_empty|string|max_length[9]'
|
||||
];
|
||||
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
|
||||
/**
|
||||
* Retrieve messages by semester and school year.
|
||||
*
|
||||
* @param string $semester
|
||||
* @param string $school_year
|
||||
* @return array
|
||||
*/
|
||||
public function getMessagesBySemesterAndYear($semester, $school_year)
|
||||
{
|
||||
return $this->where([
|
||||
'semester' => $semester,
|
||||
'school_year' => $school_year
|
||||
])->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve messages for a specific sender or receiver.
|
||||
*
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
public function getMessagesForUser($userId)
|
||||
{
|
||||
return $this->where('sender_id', $userId)
|
||||
->orWhere('reciever_id', $userId)
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a message by ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @return array|null
|
||||
*/
|
||||
public function getMessageById($id)
|
||||
{
|
||||
return $this->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a message by ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function updateMessage($id, $data)
|
||||
{
|
||||
return $this->update($id, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class CurrentFlagModel extends Model
|
||||
{
|
||||
protected $table = 'current_flag';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'student_name',
|
||||
'grade',
|
||||
'flag',
|
||||
'flag_datetime',
|
||||
'flag_state',
|
||||
'updated_by_open',
|
||||
'open_description',
|
||||
'updated_by_closed',
|
||||
'close_description',
|
||||
'action_taken',
|
||||
'updated_by_canceled',
|
||||
'cancel_description',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
// Fields that are allowed to be inserted or updated
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class DiscountUsageModel extends Model
|
||||
{
|
||||
protected $table = 'discount_usages';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'voucher_id',
|
||||
'invoice_id',
|
||||
'discount_amount',
|
||||
'school_year',
|
||||
'semester',
|
||||
'updated_by',
|
||||
'parent_id',
|
||||
'used_at'
|
||||
];
|
||||
|
||||
// Enable automatic timestamps if desired (optional: only if you add created_at/updated_at columns)
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
// Add validation rules
|
||||
protected $validationRules = [
|
||||
'voucher_id' => 'required|is_natural_no_zero',
|
||||
'invoice_id' => 'required|is_natural_no_zero',
|
||||
'user_id' => 'permit_empty|is_natural_no_zero',
|
||||
'used_at' => 'valid_date'
|
||||
];
|
||||
|
||||
protected $validationMessages = [
|
||||
'voucher_id' => [
|
||||
'required' => 'Voucher ID is required.'
|
||||
],
|
||||
'invoice_id' => [
|
||||
'required' => 'Invoice ID is required.'
|
||||
]
|
||||
];
|
||||
|
||||
public function getTotalDiscountByParentIdAndSchoolYear(int $parentId, string $schoolYear): float
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$result = $db->table('discount_usages du')
|
||||
->selectSum('du.discount_amount', 'total_discount')
|
||||
->join('invoices i', 'du.invoice_id = i.id')
|
||||
->where('i.parent_id', $parentId)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
return $result && isset($result['total_discount']) ? (float) $result['total_discount'] : 0.00;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class DiscountVoucherModel extends Model
|
||||
{
|
||||
protected $table = 'discount_vouchers';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'code',
|
||||
'discount_type',
|
||||
'discount_value',
|
||||
'max_uses',
|
||||
'times_used',
|
||||
'valid_from',
|
||||
'valid_until',
|
||||
'semester',
|
||||
'school_year',
|
||||
'is_active',
|
||||
'description', // <-- NEW
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
// Timestamps managed by CI
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
// Basic validation
|
||||
protected $validationRules = [
|
||||
'code' => 'required|alpha_numeric_punct|max_length[50]|is_unique[discount_vouchers.code,id,{id}]',
|
||||
'discount_type' => 'required|in_list[percent,fixed]',
|
||||
'discount_value' => 'required|numeric|greater_than_equal_to[0]',
|
||||
'max_uses' => 'permit_empty|is_natural',
|
||||
'valid_from' => 'permit_empty|valid_date[Y-m-d]',
|
||||
'valid_until' => 'permit_empty|valid_date[Y-m-d]',
|
||||
'is_active' => 'in_list[0,1]',
|
||||
'description' => 'permit_empty|string|max_length[1000]', // <-- NEW
|
||||
];
|
||||
|
||||
protected $validationMessages = [
|
||||
'code' => [
|
||||
'is_unique' => 'The voucher code must be unique.',
|
||||
],
|
||||
'discount_type' => [
|
||||
'in_list' => 'The discount type must be either "percent" or "fixed".',
|
||||
],
|
||||
];
|
||||
|
||||
// Normalize inputs before save
|
||||
protected $beforeInsert = ['normalizeFields', 'validateDatesOrder'];
|
||||
protected $beforeUpdate = ['normalizeFields', 'validateDatesOrder'];
|
||||
|
||||
protected function normalizeFields(array $data): array
|
||||
{
|
||||
if (!isset($data['data'])) return $data;
|
||||
|
||||
// Normalize code: keep A-Z, 0-9 and dashes, uppercased
|
||||
if (array_key_exists('code', $data['data'])) {
|
||||
$raw = (string) $data['data']['code'];
|
||||
$normalized = strtoupper(preg_replace('/[^A-Z0-9\-]/i', '', trim($raw)));
|
||||
$data['data']['code'] = $normalized;
|
||||
}
|
||||
|
||||
// Empty strings -> NULL for dates & max_uses
|
||||
foreach (['valid_from', 'valid_until'] as $f) {
|
||||
if (array_key_exists($f, $data['data'])) {
|
||||
$v = trim((string) $data['data'][$f]);
|
||||
$data['data'][$f] = ($v === '') ? null : $v;
|
||||
}
|
||||
}
|
||||
if (array_key_exists('max_uses', $data['data'])) {
|
||||
$v = $data['data']['max_uses'];
|
||||
$data['data']['max_uses'] = ($v === '' || $v === null) ? null : max(0, (int) $v);
|
||||
}
|
||||
|
||||
// Coerce booleans
|
||||
if (array_key_exists('is_active', $data['data'])) {
|
||||
$data['data']['is_active'] = (int) !!$data['data']['is_active'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function validateDatesOrder(array $data): array
|
||||
{
|
||||
if (!isset($data['data'])) return $data;
|
||||
|
||||
$from = $data['data']['valid_from'] ?? null;
|
||||
$until = $data['data']['valid_until'] ?? null;
|
||||
|
||||
if ($from && $until && $from > $until) {
|
||||
// Inject a model error; save() will fail and return errors()
|
||||
$this->validator->setError('valid_until', 'Valid Until must be the same as or after Valid From.');
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a valid voucher for a student (not expired, active, under max uses, and not used by the student).
|
||||
*/
|
||||
public function findValidVoucher(string $code, int $studentId)
|
||||
{
|
||||
$code = strtoupper(preg_replace('/\s+/', '', trim($code))); // simple normalize to match stored code
|
||||
|
||||
$builder = $this->db->table('discount_vouchers v')
|
||||
->select('v.*')
|
||||
// Left join usages for this student only
|
||||
->join('discount_usages u', 'u.voucher_id = v.id AND u.student_id = ' . (int) $studentId, 'left')
|
||||
->where('v.code', $code)
|
||||
->where('v.is_active', 1)
|
||||
// valid_from is null or <= today
|
||||
->groupStart()
|
||||
->where('v.valid_from IS NULL', null, false)
|
||||
->orWhere('v.valid_from <=', date('Y-m-d'))
|
||||
->groupEnd()
|
||||
// valid_until is null or >= today
|
||||
->groupStart()
|
||||
->where('v.valid_until IS NULL', null, false)
|
||||
->orWhere('v.valid_until >=', date('Y-m-d'))
|
||||
->groupEnd()
|
||||
// max_uses is null or times_used < max_uses (use raw to compare two columns)
|
||||
->groupStart()
|
||||
->where('v.max_uses IS NULL', null, false)
|
||||
->orWhere('v.times_used < v.max_uses', null, false)
|
||||
->groupEnd()
|
||||
// Not previously used by this student (no usage row)
|
||||
->where('u.id IS NULL', null, false);
|
||||
|
||||
return $builder->get()->getRow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class EarlyDismissalSignatureModel extends Model
|
||||
{
|
||||
protected $table = 'early_dismissal_signatures';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
protected $allowedFields = [
|
||||
'report_date',
|
||||
'school_year',
|
||||
'semester',
|
||||
'filename',
|
||||
'original_name',
|
||||
'mime_type',
|
||||
'file_size',
|
||||
'uploaded_by',
|
||||
];
|
||||
|
||||
protected $validationRules = [
|
||||
'report_date' => 'required|valid_date[Y-m-d]',
|
||||
'filename' => 'required|string',
|
||||
'school_year' => 'permit_empty|string',
|
||||
'semester' => 'permit_empty|string',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class EmailTemplateModel extends Model {
|
||||
protected $table = 'email_templates';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = ['template_key','name','subject','body','is_active'];
|
||||
|
||||
public function getActiveTemplates(): array {
|
||||
return $this->where('is_active', 1)->orderBy('template_key','asc')->findAll();
|
||||
}
|
||||
public function findByKey(string $key): ?array {
|
||||
return $this->where('template_key', $key)->where('is_active', 1)->first();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class EmergencyContactModel extends Model
|
||||
{
|
||||
protected $table = 'emergency_contacts';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'parent_id',
|
||||
'emergency_contact_name',
|
||||
'cellphone',
|
||||
'email',
|
||||
'relation',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
|
||||
/**
|
||||
* Get emergency contact by student ID.
|
||||
*
|
||||
* @param int $studentId
|
||||
* @return array
|
||||
*/
|
||||
public function getEmergencyContactsByStudentId($studentId)
|
||||
{
|
||||
return $this->where('student_id', $studentId)
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get emergency contact by parent ID.
|
||||
*
|
||||
* @param int $parentId
|
||||
* @return array
|
||||
*/
|
||||
/*
|
||||
public function getEmergencyContactsByParentId($parentId)
|
||||
{
|
||||
return $this->groupStart()
|
||||
->where('parent_id', $parentId)
|
||||
->groupEnd()
|
||||
->findAll();
|
||||
}
|
||||
*/
|
||||
/**
|
||||
* Get authorized user's emergency contact.
|
||||
*
|
||||
* @param int $authorizedUserId
|
||||
* @return array|null
|
||||
*/
|
||||
public function getEmergencyContactByAuthorizedUserId($authorizedUserId)
|
||||
{
|
||||
return $this->where('authorized_user_id', $authorizedUserId)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all emergency contacts for a specific student and their associated parents or authorized user.
|
||||
*
|
||||
* @param int $studentId
|
||||
* @return array
|
||||
*/
|
||||
public function getAllEmergencyContacts($studentId)
|
||||
{
|
||||
return $this->groupStart()
|
||||
->where('student_id', $studentId)
|
||||
->orWhere('parent_id', $studentId)
|
||||
->groupEnd()
|
||||
->findAll();
|
||||
}
|
||||
|
||||
// Function to get the emergency contact based on parent_id
|
||||
public function getEmergencyContactByParentId($parentId)
|
||||
{
|
||||
// Query to retrieve the emergency contact details using either parent_id or secondparent_id
|
||||
return $this->where('parent_id', $parentId)
|
||||
->select('emergency_contact_name, cellphone')
|
||||
->first(); // Use `first()` to get a single result
|
||||
}
|
||||
|
||||
public function getEmergencyContactsByParentId($parentId)
|
||||
{
|
||||
return $this->where('parent_id', $parentId)->findAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class EnrollmentModel extends Model
|
||||
{
|
||||
protected $table = 'enrollments';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'class_section_id',
|
||||
'parent_id',
|
||||
'school_year',
|
||||
'enrollment_date',
|
||||
'enrollment_status',
|
||||
'withdrawal_date',
|
||||
'is_withdrawn',
|
||||
'admission_status',
|
||||
'semester',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
protected $validationRules = [
|
||||
'student_id' => 'required|integer',
|
||||
'class_section_id' => 'permit_empty|integer',
|
||||
'parent_id' => 'required|integer',
|
||||
'school_year' => 'required|string|max_length[25]',
|
||||
'enrollment_date' => 'required|valid_date',
|
||||
'withdrawal_date' => 'permit_empty|valid_date',
|
||||
'is_withdrawn' => 'permit_empty|in_list[0,1]',
|
||||
'enrollment_status' => 'required|in_list[admission under review,payment pending,enrolled,withdraw under review,refund pending,withdrawn]',
|
||||
'admission_status' => 'required|in_list[pending,accepted,denied]',
|
||||
'semester' => 'permit_empty|string|max_length[25]',
|
||||
];
|
||||
|
||||
protected $validationMessages = [
|
||||
'student_id' => [
|
||||
'required' => 'Student ID is required',
|
||||
'integer' => 'Student ID must be an integer',
|
||||
],
|
||||
'class_section_id' => [
|
||||
'integer' => 'Class Section ID must be an integer',
|
||||
],
|
||||
'parent_id' => [
|
||||
'required' => 'Parent ID is required',
|
||||
'integer' => 'Parent ID must be an integer',
|
||||
],
|
||||
'school_year' => [
|
||||
'required' => 'School year is required',
|
||||
'string' => 'School year must be a string',
|
||||
'max_length' => 'School year must not exceed 25 characters',
|
||||
],
|
||||
'enrollment_date' => [
|
||||
'required' => 'Enrollment date is required',
|
||||
'valid_date' => 'Enrollment date must be a valid date',
|
||||
],
|
||||
'withdrawal_date' => [
|
||||
'valid_date' => 'Withdrawal date must be a valid date',
|
||||
],
|
||||
'is_withdrawn' => [
|
||||
'in_list' => 'Withdrawal status must be 0 (not withdrawn) or 1 (withdrawn)',
|
||||
],
|
||||
'enrollment_status' => [
|
||||
'required' => 'Enrollment status is required',
|
||||
'in_list' => 'Enrollment status must be one of: admission under review, payment pending, enrolled, withdraw under review, refund pending, withdrawn',
|
||||
],
|
||||
'admission_status' => [
|
||||
'required' => 'Admission status is required',
|
||||
'in_list' => 'Admission status must be one of: pending, accepted, denied',
|
||||
],
|
||||
'semester' => [
|
||||
'max_length' => 'Semester must not exceed 25 characters',
|
||||
],
|
||||
];
|
||||
|
||||
protected $skipValidation = false;
|
||||
|
||||
/**
|
||||
* Get all enrolled students (full student info) for a given parent.
|
||||
*/
|
||||
public function getEnrolledStudents(int $parentId, string $schoolYear = null, string $semester = null): array
|
||||
{
|
||||
$builder = $this->select('students.*')
|
||||
->join('students', 'students.id = enrollments.student_id')
|
||||
->where('enrollments.parent_id', $parentId);
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('enrollments.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if ($semester) {
|
||||
$builder->where('enrollments.semester', $semester);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get student basic info (ID, name, grade) for a given parent where status is enrolled or Payment pending.
|
||||
*/
|
||||
public function getenrolledStudentDetails(int $parentId, string $schoolYear = null): array
|
||||
{
|
||||
$builder = $this->db->table('enrollments')
|
||||
->select('students.id, students.firstname, students.lastname, students.grade_level')
|
||||
->join('students', 'students.id = enrollments.student_id')
|
||||
->where('enrollments.parent_id', $parentId)
|
||||
->groupStart()
|
||||
->where('enrollments.enrollment_status', 'enrolled')
|
||||
->orWhere('enrollments.enrollment_status', 'payment pending')
|
||||
->groupEnd();
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('enrollments.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest enrollment_status for a student in a given school year.
|
||||
*
|
||||
* @param int $studentId
|
||||
* @param string $schoolYear e.g. "2025-2026"
|
||||
* @return string|null e.g. "enrolled", "pending", "denied", or null if not found
|
||||
*/
|
||||
public function getEnrollmentStatus(int $studentId, string $schoolYear): ?string
|
||||
{
|
||||
$row = $this->select('enrollment_status')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
// prefer most recently updated, then most recent enrollment_date, then newest id
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->orderBy('enrollment_date', 'DESC')
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
|
||||
return $row['enrollment_status'] ?? null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class EventChargesModel extends Model
|
||||
{
|
||||
protected $table = 'event_charges';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'event_id',
|
||||
'parent_id',
|
||||
'student_id',
|
||||
'participation',
|
||||
'charged',
|
||||
'semester',
|
||||
'school_year',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
protected $returnType = 'array'; // Or 'object' if preferred
|
||||
|
||||
public function getChargesWithEventInfo($parentId = null, $schoolYear = null, $semester = null)
|
||||
{
|
||||
$builder = $this->select('event_charges.*, events.event_name, events.amount')
|
||||
->join('events', 'events.id = event_charges.event_id', 'left');
|
||||
|
||||
if ($parentId) {
|
||||
$builder->where('event_charges.parent_id', $parentId);
|
||||
}
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('event_charges.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if ($semester) {
|
||||
$builder->where('event_charges.semester', $semester);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class EventModel extends Model
|
||||
{
|
||||
protected $table = 'events';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'event_name',
|
||||
'event_category',
|
||||
'description',
|
||||
'amount',
|
||||
'flyer',
|
||||
'expiration_date',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_by',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
/**
|
||||
* Get all upcoming events (not expired).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUpcomingEvents($schoolYear = null)
|
||||
{
|
||||
$builder = $this->where('expiration_date >=', date('Y-m-d'));
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $builder->orderBy('expiration_date', 'ASC')->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events by name (partial match).
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $schoolYear
|
||||
* @return array
|
||||
*/
|
||||
public function getEventsByName(string $name, $schoolYear = null)
|
||||
{
|
||||
$builder = $this->like('event_name', $name);
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $builder->orderBy('expiration_date', 'ASC')->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all events.
|
||||
*
|
||||
* @param string|null $schoolYear
|
||||
* @return array
|
||||
*/
|
||||
public function getAllEvents($schoolYear = null)
|
||||
{
|
||||
$builder = $this;
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder = $builder->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $builder->orderBy('created_at', 'DESC')->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new event.
|
||||
*
|
||||
* @param array $data
|
||||
* @return int|string|false
|
||||
*/
|
||||
public function addEvent(array $data)
|
||||
{
|
||||
return $this->insert($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events by expiration date.
|
||||
*
|
||||
* @param string $date
|
||||
* @param string|null $schoolYear
|
||||
* @return array
|
||||
*/
|
||||
public function getEventsByDate($date, $schoolYear = null)
|
||||
{
|
||||
$builder = $this->where('expiration_date', $date);
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all active (not expired) events.
|
||||
*
|
||||
* @param string|null $schoolYear
|
||||
* @param string|null $semester
|
||||
* @return array
|
||||
*/
|
||||
public function getActiveEvents($schoolYear = null, $semester = null)
|
||||
{
|
||||
$builder = $this->where('expiration_date >=', date('Y-m-d'));
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if ($semester) {
|
||||
$builder->where('semester', $semester);
|
||||
}
|
||||
|
||||
return $builder->orderBy('expiration_date', 'ASC')->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event by ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string|null $schoolYear
|
||||
* @return array|null
|
||||
*/
|
||||
public function getEvent($id, $schoolYear = null)
|
||||
{
|
||||
$builder = $this->where('id', $id);
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $builder->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update event by ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @param array $data
|
||||
* @param string|null $schoolYear
|
||||
* @return bool
|
||||
*/
|
||||
public function updateEvent($id, $data, $schoolYear = null)
|
||||
{
|
||||
if ($schoolYear) {
|
||||
// Ensure only updates for this school year
|
||||
$this->where('id', $id)->where('school_year', $schoolYear)->set($data)->update();
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->update($id, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete event by ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string|null $schoolYear
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteEvent($id, $schoolYear = null)
|
||||
{
|
||||
if ($schoolYear) {
|
||||
return $this->where('id', $id)->where('school_year', $schoolYear)->delete();
|
||||
}
|
||||
|
||||
return $this->delete($id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ExamDraftModel extends Model
|
||||
{
|
||||
protected $table = 'exam_drafts';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'teacher_id',
|
||||
'class_section_id',
|
||||
'semester',
|
||||
'school_year',
|
||||
'exam_type',
|
||||
'draft_title',
|
||||
'description',
|
||||
'teacher_file',
|
||||
'teacher_filename',
|
||||
'status',
|
||||
'admin_id',
|
||||
'is_legacy',
|
||||
'admin_comments',
|
||||
'reviewed_at',
|
||||
'final_file',
|
||||
'final_filename',
|
||||
'final_pdf_file',
|
||||
'version',
|
||||
'previous_draft_id',
|
||||
];
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected bool $updateOnlyChanged = false; // force updates even if CI thinks nothing changed
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ExamModel extends Model
|
||||
{
|
||||
protected $table = 'exams';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
'class_section_id',
|
||||
'exam_name',
|
||||
'created_at'
|
||||
];
|
||||
|
||||
// Timestamps
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = ''; // Set this if you later add an `updated_at` column
|
||||
|
||||
protected $returnType = 'array'; // You can change this to 'object' if preferred
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ExpenseModel extends Model
|
||||
{
|
||||
protected $table = 'expenses';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'category',
|
||||
'amount',
|
||||
'receipt_path',
|
||||
'description',
|
||||
'retailor',
|
||||
'date_of_purchase',
|
||||
'purchased_by',
|
||||
'added_by',
|
||||
'school_year',
|
||||
'semester',
|
||||
'status',
|
||||
'status_reason',
|
||||
'reimbursement_id',
|
||||
'approved_by',
|
||||
'updated_by'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
protected $validationRules = [
|
||||
// Add your validation rules here as needed
|
||||
];
|
||||
|
||||
public function getReimbursedExpensesWithDetails(array $filters = [])
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('expenses');
|
||||
$builder->select('expenses.*,
|
||||
r.id AS reimb_id,
|
||||
r.amount AS reimb_amount, r.reimbursement_method, r.check_number,
|
||||
r.receipt_path AS reimb_receipt, r.status AS reimb_status,
|
||||
r.school_year, r.semester, r.created_at AS reimb_created_at,
|
||||
r.reimbursed_to AS reimb_recipient_id,
|
||||
reimb.firstname AS reimb_firstname, reimb.lastname AS reimb_lastname,
|
||||
approver.firstname AS approver_firstname, approver.lastname AS approver_lastname');
|
||||
$builder->join('reimbursements r', 'r.expense_id = expenses.id', 'inner');
|
||||
$builder->join('users reimb', 'reimb.id = r.reimbursed_to', 'left');
|
||||
$builder->join('users approver', 'approver.id = r.approved_by', 'left');
|
||||
|
||||
if (!empty($filters['school_year'])) {
|
||||
$builder->where('r.school_year', $filters['school_year']);
|
||||
}
|
||||
if (!empty($filters['semester'])) {
|
||||
$builder->where('r.semester', $filters['semester']);
|
||||
}
|
||||
if (!empty($filters['status'])) {
|
||||
$builder->where('r.status', $filters['status']);
|
||||
}
|
||||
if (!empty($filters['user_id'])) {
|
||||
$builder->where('r.reimbursed_to', $filters['user_id']);
|
||||
}
|
||||
|
||||
$builder->orderBy('r.created_at', 'DESC');
|
||||
|
||||
return $builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class FamilyCommPrefModel extends Model {
|
||||
protected $table = 'family_comm_prefs';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = ['family_id','category','via_email','via_sms','cc_all_guardians'];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class FamilyGuardianModel extends Model {
|
||||
protected $table = 'family_guardians';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = ['family_id','user_id','relation','is_primary','receive_emails','receive_sms','custody_notes'];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class FamilyModel extends Model {
|
||||
protected $table = 'families';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'family_code','household_name','address_line1','address_line2','city','state','postal_code',
|
||||
'country','primary_phone','preferred_lang','preferred_contact_method','is_active'
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class FamilyStudentModel extends Model {
|
||||
protected $table = 'family_students';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = ['family_id','student_id','is_primary_home','notes'];
|
||||
|
||||
public function getFamiliesForStudent(int $studentId): array {
|
||||
$db = \Config\Database::connect();
|
||||
$sql = "SELECT f.* , fs.is_primary_home\n FROM family_students fs\n JOIN families f ON f.id = fs.family_id\n WHERE fs.student_id = ? AND f.is_active = 1\n ORDER BY fs.is_primary_home DESC, f.household_name";
|
||||
return $db->query($sql, [$studentId])->getResultArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class FinalExamModel extends Model
|
||||
{
|
||||
protected $table = 'final_exam';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
'class_section_id',
|
||||
'updated_by',
|
||||
'score',
|
||||
'comment',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
|
||||
protected array $casts = [];
|
||||
protected array $castHandlers = [];
|
||||
|
||||
// Dates
|
||||
protected $useTimestamps = false;
|
||||
protected $dateFormat = 'datetime';
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
|
||||
// Validation
|
||||
protected $validationRules = [];
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
protected $cleanValidationRules = true;
|
||||
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = [];
|
||||
protected $afterInsert = [];
|
||||
protected $beforeUpdate = [];
|
||||
protected $afterUpdate = [];
|
||||
protected $beforeFind = [];
|
||||
protected $afterFind = [];
|
||||
protected $beforeDelete = [];
|
||||
protected $afterDelete = [];
|
||||
|
||||
public function getFinalExamScore($studentId, string $semester, $schoolYear, ?int $classSectionId = null)
|
||||
{
|
||||
$builder = $this->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($classSectionId !== null) {
|
||||
$builder->where('class_section_id', $classSectionId);
|
||||
}
|
||||
|
||||
$result = $builder->first();
|
||||
|
||||
return $result ? $result['score'] : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class FinalScoreModel extends Model
|
||||
{
|
||||
protected $table = 'final_score';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
'class_section_id',
|
||||
'teacher_id',
|
||||
'score',
|
||||
'score_letter',
|
||||
'comment',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
// Function to get the final exam score for a specific student, semester, and school year
|
||||
public function getFinalExamScore($studentId, $semester, $schoolYear)
|
||||
{
|
||||
// Query the final_score table for the final exam score based on student ID, semester, and school year
|
||||
$result = $this->select('score') // We only need the score
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first(); // Get the first matching result
|
||||
|
||||
// Return the score, or null if no result is found
|
||||
return $result ? $result['score'] : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class FlagModel extends Model
|
||||
{
|
||||
protected $table = 'flag';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'student_name',
|
||||
'grade',
|
||||
'flag',
|
||||
'flag_datetime',
|
||||
'flag_state',
|
||||
'updated_by_open',
|
||||
'open_description',
|
||||
'updated_by_closed',
|
||||
'close_description',
|
||||
'action_taken',
|
||||
'updated_by_canceled',
|
||||
'cancel_description',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class GradingLockModel extends Model
|
||||
{
|
||||
protected $table = 'grading_locks';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = [
|
||||
'class_section_id',
|
||||
'semester',
|
||||
'school_year',
|
||||
'is_locked',
|
||||
'locked_by',
|
||||
'locked_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
public function getLock(int $classSectionId, string $semester, string $schoolYear): ?array
|
||||
{
|
||||
if ($classSectionId <= 0 || $semester === '' || $schoolYear === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
}
|
||||
|
||||
public function isLocked(int $classSectionId, string $semester, string $schoolYear): bool
|
||||
{
|
||||
$row = $this->getLock($classSectionId, $semester, $schoolYear);
|
||||
return !empty($row['is_locked']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class HomeworkModel extends Model
|
||||
{
|
||||
protected $table = 'homework';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
'class_section_id',
|
||||
'updated_by',
|
||||
'homework_index',
|
||||
'score',
|
||||
'comment',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
|
||||
protected array $casts = [];
|
||||
protected array $castHandlers = [];
|
||||
|
||||
// Dates
|
||||
protected $useTimestamps = false;
|
||||
protected $dateFormat = 'datetime';
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
|
||||
// Validation
|
||||
protected $validationRules = [];
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
protected $cleanValidationRules = true;
|
||||
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = [];
|
||||
protected $afterInsert = [];
|
||||
protected $beforeUpdate = [];
|
||||
protected $afterUpdate = [];
|
||||
protected $beforeFind = [];
|
||||
protected $afterFind = [];
|
||||
protected $beforeDelete = [];
|
||||
protected $afterDelete = [];
|
||||
|
||||
/**
|
||||
* Calculates the average homework score for a student
|
||||
*
|
||||
* @param int $studentId The student ID
|
||||
* @param string $semester The semester (e.g., 'Fall', 'Spring')
|
||||
* @param string $schoolYear The school year (e.g., '2023-2024')
|
||||
* @param int|null $classSectionId Optional class section filter
|
||||
* @return float|null The average score (null if no homework found)
|
||||
*/
|
||||
public function getAverageHomeworkScore(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): ?float
|
||||
{
|
||||
// Pull all scores, then compute based only on non-blank numeric entries
|
||||
$builder = $this->db->table('homework');
|
||||
$builder->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($classSectionId !== null) {
|
||||
$builder->where('class_section_id', $classSectionId);
|
||||
}
|
||||
|
||||
$rows = $builder->get()->getResultArray();
|
||||
if (empty($rows)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$totalScore = 0.0;
|
||||
$scoreCount = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$score = $row['score'] ?? null;
|
||||
if ($score === null || (is_string($score) && trim($score) === '') || $score === '' || !is_numeric($score)) {
|
||||
continue;
|
||||
}
|
||||
$totalScore += (float) $score;
|
||||
$scoreCount++;
|
||||
}
|
||||
|
||||
if ($scoreCount === 0) {
|
||||
// All entries are blank/null
|
||||
return null;
|
||||
}
|
||||
|
||||
return round($totalScore / $scoreCount, 2);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class InventoryCategoryModel extends Model
|
||||
{
|
||||
protected $table = 'inventory_categories';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'type',
|
||||
'name',
|
||||
'description',
|
||||
'grade_min',
|
||||
'grade_max',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
public function optionsForType(string $type): array
|
||||
{
|
||||
return $this->select('id, name, description, grade_min, grade_max')
|
||||
->where('type', $type)
|
||||
->orderBy('name', 'ASC')
|
||||
->findAll(); // returnType already 'array'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class InventoryItemModel extends Model
|
||||
{
|
||||
protected $table = 'inventory_items';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
// IMPORTANT: include everything you ever update/insert
|
||||
protected $allowedFields = [
|
||||
'type',
|
||||
'category_id',
|
||||
'name',
|
||||
'description',
|
||||
'quantity', // <- recalcQuantity updates this
|
||||
'unit',
|
||||
'condition',
|
||||
'isbn',
|
||||
'edition',
|
||||
'sku',
|
||||
'notes',
|
||||
|
||||
// academic tags
|
||||
'semester',
|
||||
'school_year',
|
||||
|
||||
// audit
|
||||
'updated_by',
|
||||
|
||||
// classroom state buckets
|
||||
'good_qty',
|
||||
'needs_repair_qty',
|
||||
'need_replace_qty',
|
||||
'cannot_find_qty',
|
||||
];
|
||||
|
||||
|
||||
protected $validationRules = [
|
||||
'type' => 'required|in_list[classroom,book,office,kitchen]',
|
||||
'name' => 'required|min_length[2]',
|
||||
'quantity' => 'permit_empty|integer',
|
||||
'unit_price' => 'permit_empty|decimal',
|
||||
'semester' => 'permit_empty|in_list[Spring,Fall]',
|
||||
'school_year' => 'permit_empty|max_length[16]', // e.g. 2025-2026
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class InventoryMovementModel extends Model
|
||||
{
|
||||
protected $table = 'inventory_movements';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'item_id','qty_change','movement_type','reason','note',
|
||||
'semester','school_year',
|
||||
'performed_by','teacher_id','student_id','class_section_id',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class InvoiceEventModel extends Model
|
||||
{
|
||||
protected $table = 'invoice_event';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'invoice_id',
|
||||
'event_name',
|
||||
'description',
|
||||
'amount',
|
||||
'semester',
|
||||
'school_year',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
protected $returnType = 'array'; // or 'object' if you prefer
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class InvoiceModel extends Model
|
||||
{
|
||||
protected $table = 'invoices';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
|
||||
protected $allowedFields = [
|
||||
'parent_id',
|
||||
'invoice_number',
|
||||
'total_amount',
|
||||
'school_year',
|
||||
'semester',
|
||||
'balance',
|
||||
'paid_amount',
|
||||
'has_discount',
|
||||
'issue_date', // DATETIME
|
||||
'due_date', // DATETIME (nullable)
|
||||
'status',
|
||||
'description',
|
||||
'created_at', // DATETIME (DB default/trigger)
|
||||
'updated_at', // DATETIME (DB default/trigger)
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
// Your table handles timestamps (defaults/triggers), keep CI auto off
|
||||
protected $useTimestamps = false;
|
||||
|
||||
// --- Validate as DATETIME strings like "YYYY-MM-DD HH:MM:SS" ---
|
||||
protected $validationRules = [
|
||||
// allow update without tripping uniqueness
|
||||
'invoice_number' => 'required|is_unique[invoices.invoice_number,id,{id}]',
|
||||
'total_amount' => 'required|decimal',
|
||||
'balance' => 'permit_empty|decimal',
|
||||
'issue_date' => 'required|valid_date[Y-m-d H:i:s]',
|
||||
'due_date' => 'permit_empty|valid_date[Y-m-d H:i:s]',
|
||||
'status' => 'required|string|max_length[50]',
|
||||
];
|
||||
|
||||
|
||||
protected $validationMessages = [
|
||||
'invoice_number' => [
|
||||
'required' => 'The invoice number is required.',
|
||||
'is_unique' => 'The invoice number must be unique.',
|
||||
],
|
||||
'total_amount' => [
|
||||
'required' => 'The total amount is required.',
|
||||
'decimal' => 'The total amount must be a valid decimal.',
|
||||
],
|
||||
'balance' => [
|
||||
'decimal' => 'The balance must be a valid decimal.',
|
||||
],
|
||||
'issue_date' => [
|
||||
'required' => 'The issue date is required.',
|
||||
'valid_date' => 'The issue date must be a valid datetime (YYYY-MM-DD HH:MM:SS).',
|
||||
],
|
||||
'due_date' => [
|
||||
'valid_date' => 'The due date must be a valid datetime (YYYY-MM-DD HH:MM:SS).',
|
||||
],
|
||||
'status' => [
|
||||
'required' => 'The status is required.',
|
||||
'max_length' => 'The status must not exceed 50 characters.',
|
||||
],
|
||||
];
|
||||
|
||||
// --- Normalize any incoming date/datetime into UTC 'Y-m-d H:i:s' ---
|
||||
protected $beforeInsert = ['normalizeDateTimes'];
|
||||
protected $beforeUpdate = ['normalizeDateTimes'];
|
||||
|
||||
protected function normalizeDateTimes(array $data): array
|
||||
{
|
||||
foreach (['issue_date', 'due_date', 'created_at', 'updated_at'] as $field) {
|
||||
if (array_key_exists($field, $data['data'])) {
|
||||
$val = $data['data'][$field];
|
||||
if ($val === null || $val === '' || $val === '0000-00-00' || $val === '0000-00-00 00:00:00') {
|
||||
// keep NULL/empty for nullable fields
|
||||
$data['data'][$field] = ($field === 'due_date') ? null : $val;
|
||||
continue;
|
||||
}
|
||||
$normalized = $this->toUtcDateTimeString($val);
|
||||
if ($normalized !== null) {
|
||||
$data['data'][$field] = $normalized;
|
||||
}
|
||||
// else leave as-is; validation will catch invalid format
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function toUtcDateTimeString($raw): ?string
|
||||
{
|
||||
if ($raw instanceof \DateTimeInterface) {
|
||||
$dt = (new \DateTimeImmutable('@' . $raw->getTimestamp()))
|
||||
->setTimezone(new \DateTimeZone('UTC'));
|
||||
return $dt->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$s = trim((string) $raw);
|
||||
if ($s === '') return null;
|
||||
|
||||
// Try strict formats first
|
||||
$utc = new \DateTimeZone('UTC');
|
||||
|
||||
$dt = \DateTime::createFromFormat('Y-m-d H:i:s', $s, $utc);
|
||||
if ($dt !== false) {
|
||||
$dt->setTimezone($utc);
|
||||
return $dt->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$d = \DateTime::createFromFormat('Y-m-d', $s, $utc);
|
||||
if ($d !== false) {
|
||||
// If date-only sneaks in, store at 12:00:00 to avoid boundary confusion
|
||||
$d->setTime(12, 0, 0);
|
||||
$d->setTimezone($utc);
|
||||
return $d->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
// Fallback parser (may accept ISO8601, etc.)
|
||||
try {
|
||||
$flex = new \DateTime($s);
|
||||
$flex->setTimezone($utc);
|
||||
return $flex->format('Y-m-d H:i:s');
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------- Queries -----------------------
|
||||
|
||||
public function getInvoicesByUserId($userId, $schoolYear = null)
|
||||
{
|
||||
$builder = $this->db->table($this->table . ' i')
|
||||
->select('i.*')
|
||||
->select('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount', false)
|
||||
->where('i.parent_id', $userId);
|
||||
if ($schoolYear !== null) {
|
||||
$builder->where('i.school_year', $schoolYear);
|
||||
}
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function updateInvoiceStatus($invoiceId, $status)
|
||||
{
|
||||
return $this->update($invoiceId, ['status' => $status]);
|
||||
}
|
||||
|
||||
public function getUnpaidInvoices()
|
||||
{
|
||||
return $this->whereIn('status', ['Unpaid', 'Partially Paid', 'unpaid', 'partially paid'])
|
||||
->orderBy('due_date', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
public function getLatestInvoiceTotalAmount($parentId)
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->select('total_amount')
|
||||
->get()
|
||||
->getRow('total_amount');
|
||||
}
|
||||
|
||||
public function getLatestInvoicePaidAmount($parentId)
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->select('paid_amount')
|
||||
->get()
|
||||
->getRow('paid_amount');
|
||||
}
|
||||
|
||||
public function getLatestInvoiceBalance($parentId)
|
||||
{
|
||||
$invoice = $this->where('parent_id', $parentId)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->select('balance')
|
||||
->get()
|
||||
->getRow();
|
||||
|
||||
return $invoice ? $invoice->balance : null;
|
||||
}
|
||||
|
||||
public function getInvoiceEventCharges($invoiceId)
|
||||
{
|
||||
return $this->db->table('invoice_event')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
public function getInvoicesByParentId($parentId, $schoolYear = null)
|
||||
{
|
||||
$builder = $this->db->table($this->table . ' i')
|
||||
->select('i.*')
|
||||
->select('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount', false)
|
||||
->where('i.parent_id', $parentId);
|
||||
if (!empty($schoolYear)) {
|
||||
$builder->where('i.school_year', $schoolYear);
|
||||
}
|
||||
return $builder->orderBy('i.created_at', 'DESC')->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function getLatestInvoiceBalanceByYear($parentId, $schoolYear)
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->select('total_amount, paid_amount, balance, updated_at')
|
||||
->get()
|
||||
->getRowArray();
|
||||
}
|
||||
|
||||
public function getDiscountedInvoicesByParent($parentId)
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
->where('has_discount', 1)
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* Query helpers
|
||||
* ======================= */
|
||||
|
||||
|
||||
/**
|
||||
* Multi-parent variant (useful when preloading for all parents).
|
||||
*/
|
||||
public function getInvoicesByUserIds(array $parentIds, string $schoolYear): array
|
||||
{
|
||||
if (empty($parentIds)) return [];
|
||||
$builder = $this->db->table($this->table . ' i')
|
||||
->select('i.id,i.parent_id,i.invoice_number,i.status,i.balance,i.total_amount,i.issue_date,i.due_date')
|
||||
->select('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount', false)
|
||||
->whereIn('i.parent_id', array_map('intval', $parentIds))
|
||||
->where('i.school_year', $schoolYear)
|
||||
->orderBy('i.issue_date', 'DESC')
|
||||
->orderBy('i.id', 'DESC');
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* Additional charge math
|
||||
* ======================= */
|
||||
|
||||
/**
|
||||
* Atomically add an extra amount to additional_charge (and optionally totals).
|
||||
* Adjust total_amount and balance too (comment out if you don't want that).
|
||||
*/
|
||||
public function applyAdditionalCharge(int $invoiceId, float $amount): bool
|
||||
{
|
||||
$val = number_format(abs($amount), 2, '.', '');
|
||||
$ok = $this->db->query(
|
||||
"UPDATE {$this->table}
|
||||
SET total_amount = total_amount + {$val},
|
||||
balance = balance + {$val}
|
||||
WHERE id = ?", [$invoiceId]
|
||||
);
|
||||
return $ok && $this->db->affectedRows() > 0;
|
||||
}
|
||||
|
||||
public function deductAdditionalCharge(int $invoiceId, float $amount): bool
|
||||
{
|
||||
$val = number_format(abs($amount), 2, '.', '');
|
||||
$ok = $this->db->query(
|
||||
"UPDATE {$this->table}
|
||||
SET total_amount = GREATEST(total_amount - {$val}, 0),
|
||||
balance = GREATEST(balance - {$val}, 0)
|
||||
WHERE id = ?", [$invoiceId]
|
||||
);
|
||||
return $ok && $this->db->affectedRows() > 0;
|
||||
}
|
||||
|
||||
|
||||
public function getAllInvoicesByUserIds(array $parentIds, string $schoolYear): array
|
||||
{
|
||||
if (empty($parentIds)) return [];
|
||||
$parentIds = array_map('intval', $parentIds);
|
||||
$builder = $this->db->table($this->table . ' i')
|
||||
->select('i.id, i.parent_id, i.invoice_number, i.status, i.balance, i.issue_date')
|
||||
->select('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount', false)
|
||||
->whereIn('i.parent_id', $parentIds)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->orderBy('i.issue_date', 'DESC')
|
||||
->orderBy('i.id', 'DESC');
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reverse an extra amount from additional_charge (and totals), never below 0.
|
||||
*/
|
||||
public function reverseAdditionalCharge(int $invoiceId, float $amount): bool
|
||||
{
|
||||
$amount = round($amount, 2);
|
||||
$tb = $this->db->table($this->table);
|
||||
$val = $this->db->escape($amount);
|
||||
return $tb->set('total_amount', "GREATEST(total_amount - $val, 0)", false)
|
||||
->set('balance', "GREATEST(balance - $val, 0)", false)
|
||||
->where('id', $invoiceId)
|
||||
->update();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class InvoiceStudentListModel extends Model
|
||||
{
|
||||
// Specify the table name
|
||||
protected $table = 'invoice_students_list';
|
||||
|
||||
// Primary key for the table
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// Specify which fields are allowed for mass assignment
|
||||
protected $allowedFields = [
|
||||
'invoice_id',
|
||||
'student_id',
|
||||
'student_firstname',
|
||||
'student_lastname',
|
||||
'school_id',
|
||||
'enrolled',
|
||||
'school_year',
|
||||
'semester',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
// Enable auto-incrementing primary key
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
// Specify return type as an array
|
||||
protected $returnType = 'array';
|
||||
|
||||
// Automatically set `created_at` and `updated_at` fields
|
||||
protected $useTimestamps = true;
|
||||
|
||||
// Specify the names of the timestamp fields
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class IpAttemptModel extends Model
|
||||
{
|
||||
protected $table = 'ip_attempts';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'ip_address',
|
||||
'attempts',
|
||||
'last_attempt_at',
|
||||
'semester',
|
||||
'school_year',
|
||||
'blocked_until'
|
||||
];
|
||||
|
||||
// Automatically handle timestamps
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
// Validation rules
|
||||
protected $validationRules = [
|
||||
'ip_address' => 'required|valid_ip|max_length[45]',
|
||||
'attempts' => 'required|integer',
|
||||
'last_attempt_at' => 'required|valid_date',
|
||||
'blocked_until' => 'permit_empty|valid_date'
|
||||
];
|
||||
|
||||
// Method to get IP attempt data by IP address
|
||||
public function getAttemptByIp($ip)
|
||||
{
|
||||
return $this->where('ip_address', $ip)->first();
|
||||
}
|
||||
|
||||
// Method to update the IP attempt data
|
||||
public function updateIpAttempt($ip, $data)
|
||||
{
|
||||
return $this->where('ip_address', $ip)->set($data)->update();
|
||||
}
|
||||
|
||||
// Method to insert new IP attempt data
|
||||
public function insertIpAttempt($data)
|
||||
{
|
||||
return $this->insert($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class LateSlipLogModel extends Model
|
||||
{
|
||||
protected $table = 'late_slip_logs';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useTimestamps = false; // we control printed_at explicitly
|
||||
protected $returnType = 'array';
|
||||
protected $allowedFields = [
|
||||
'school_year',
|
||||
'semester',
|
||||
'student_name',
|
||||
'slip_date',
|
||||
'time_in',
|
||||
'grade',
|
||||
'reason',
|
||||
'admin_name',
|
||||
'printed_by',
|
||||
'printed_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* Best-effort insert of a single late slip print log.
|
||||
* Swallows DB errors to avoid blocking printing.
|
||||
*
|
||||
* @param array $data expects keys: school_year, student_name, slip_date (Y-m-d), time_in (H:i:s), grade, reason, admin_name
|
||||
* @param int|null $printedBy user id of the admin performing the print
|
||||
* @return bool success (best-effort)
|
||||
*/
|
||||
public function logSlip(array $data, ?int $printedBy): bool
|
||||
{
|
||||
$row = [
|
||||
'school_year' => (string)($data['school_year'] ?? ''),
|
||||
'semester' => (string)($data['semester'] ?? ''),
|
||||
'student_name' => (string)($data['student_name'] ?? ''),
|
||||
'slip_date' => $data['slip_date'] ?? null,
|
||||
'time_in' => $data['time_in'] ?? null,
|
||||
'grade' => (string)($data['grade'] ?? ''),
|
||||
'reason' => (string)($data['reason'] ?? ''),
|
||||
'admin_name' => (string)($data['admin_name'] ?? ''),
|
||||
'printed_by' => $printedBy,
|
||||
'printed_at' => utc_now(),
|
||||
];
|
||||
|
||||
try {
|
||||
return (bool) $this->insert($row);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'LateSlipLogModel::logSlip failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class LoginActivityModel extends Model
|
||||
{
|
||||
protected $table = 'login_activity';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'user_id',
|
||||
'email',
|
||||
'login_time',
|
||||
'logout_time',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'school_year',
|
||||
'semester',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
public function getLastActivities($limit = 4)
|
||||
{
|
||||
return $this->orderBy('login_time', 'DESC')->findAll($limit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ManualPaymentModel extends Model
|
||||
{
|
||||
protected $table = 'manual_payments';
|
||||
protected $allowedFields = [
|
||||
'invoice_number',
|
||||
'amount',
|
||||
'payment_method',
|
||||
'reference',
|
||||
'proof_path',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at'];
|
||||
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MessageModel extends Model
|
||||
{
|
||||
protected $table = 'messages';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'sender_id',
|
||||
'recipient_id',
|
||||
'subject',
|
||||
'message',
|
||||
'sent_datetime',
|
||||
'read_status',
|
||||
'read_datetime',
|
||||
'message_number',
|
||||
'priority',
|
||||
'attachment',
|
||||
'status',
|
||||
'semester', // Added field
|
||||
'school_year' // Added field
|
||||
];
|
||||
|
||||
protected $useTimestamps = false; // Since you're manually handling date fields
|
||||
protected $returnType = 'array';
|
||||
|
||||
/**
|
||||
* Get unread messages for a specific recipient.
|
||||
*
|
||||
* @param int $recipientId
|
||||
* @return array
|
||||
*/
|
||||
public function getUnreadMessages($recipientId)
|
||||
{
|
||||
return $this->where('recipient_id', $recipientId)
|
||||
->where('read_status', 0)
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a message as read and update the read_datetime.
|
||||
*
|
||||
* @param int $messageId
|
||||
* @return bool
|
||||
*/
|
||||
public function markAsRead($messageId)
|
||||
{
|
||||
return $this->update($messageId, [
|
||||
'read_status' => 1,
|
||||
'read_datetime' => utc_now() // Set the current datetime when marked as read
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages by priority.
|
||||
*
|
||||
* @param string $priority
|
||||
* @return array
|
||||
*/
|
||||
public function getMessagesByPriority($priority = 'normal')
|
||||
{
|
||||
return $this->where('priority', $priority)
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages by status.
|
||||
*
|
||||
* @param string $status
|
||||
* @return array
|
||||
*/
|
||||
public function getMessagesByStatus($status = 'sent')
|
||||
{
|
||||
return $this->where('status', $status)
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inbox messages for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
public function getInboxMessages($userId)
|
||||
{
|
||||
return $this->where('recipient_id', $userId)
|
||||
->where('status', 'received')
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sent messages for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
public function getSentMessages($userId)
|
||||
{
|
||||
return $this->where('sender_id', $userId)
|
||||
->where('status', 'sent')
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get draft messages for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
public function getDraftMessages($userId)
|
||||
{
|
||||
return $this->where('sender_id', $userId)
|
||||
->where('status', 'draft')
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trashed messages for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
public function getTrashedMessages($userId)
|
||||
{
|
||||
return $this->groupStart()
|
||||
->where('sender_id', $userId)
|
||||
->orWhere('recipient_id', $userId)
|
||||
->groupEnd()
|
||||
->where('status', 'trashed')
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages for a specific semester and school year.
|
||||
*
|
||||
* @param string $semester
|
||||
* @param string|null $school_year
|
||||
* @return array
|
||||
*/
|
||||
public function getMessagesBySemesterAndYear($semester, $school_year = null)
|
||||
{
|
||||
$builder = $this->where('semester', $semester);
|
||||
if ($school_year) {
|
||||
$builder->where('school_year', $school_year);
|
||||
}
|
||||
return $builder->orderBy('sent_datetime', 'DESC')->findAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MidtermExamModel extends Model
|
||||
{
|
||||
protected $table = 'midterm_exam';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
'class_section_id',
|
||||
'updated_by',
|
||||
'score',
|
||||
'comment',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
|
||||
protected array $casts = [];
|
||||
protected array $castHandlers = [];
|
||||
|
||||
// Dates
|
||||
protected $useTimestamps = false;
|
||||
protected $dateFormat = 'datetime';
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
|
||||
// Validation
|
||||
protected $validationRules = [];
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
protected $cleanValidationRules = true;
|
||||
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = [];
|
||||
protected $afterInsert = [];
|
||||
protected $beforeUpdate = [];
|
||||
protected $afterUpdate = [];
|
||||
protected $beforeFind = [];
|
||||
protected $afterFind = [];
|
||||
protected $beforeDelete = [];
|
||||
protected $afterDelete = [];
|
||||
|
||||
// Function to get the midterm exam score for a specific student, semester, school year, and class section
|
||||
public function getMidtermExamScore($studentId, ?string $semester, $schoolYear, ?int $classSectionId = null)
|
||||
{
|
||||
$builder = $this->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($semester !== null && $semester !== '') {
|
||||
$builder->where('semester', $semester);
|
||||
}
|
||||
if ($classSectionId !== null) {
|
||||
$builder->where('class_section_id', $classSectionId);
|
||||
}
|
||||
|
||||
$result = $builder->first();
|
||||
|
||||
return $result ? $result['score'] : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MissingScoreOverrideModel extends Model
|
||||
{
|
||||
protected $table = 'missing_score_overrides';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'class_section_id',
|
||||
'semester',
|
||||
'school_year',
|
||||
'item_type',
|
||||
'item_index',
|
||||
'is_allowed',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
public function getOverridesMap(int $classSectionId, string $semester, string $schoolYear, string $itemType): array
|
||||
{
|
||||
if ($classSectionId <= 0 || $semester === '' || $schoolYear === '' || $itemType === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->select('student_id, item_index')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('item_type', $itemType)
|
||||
->where('is_allowed', 1)
|
||||
->findAll();
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$sid = (int) ($row['student_id'] ?? 0);
|
||||
$idx = isset($row['item_index']) ? (int) $row['item_index'] : 0;
|
||||
if ($sid > 0) {
|
||||
$map[$sid][$idx] = true;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function replaceOverrides(
|
||||
int $classSectionId,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
string $itemType,
|
||||
array $studentIds,
|
||||
?array $itemIndexes,
|
||||
array $checkedItems,
|
||||
?int $updatedBy,
|
||||
bool $nullIndex = false
|
||||
): void {
|
||||
if ($classSectionId <= 0 || $semester === '' || $schoolYear === '' || $itemType === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$builder = $this->builder();
|
||||
$builder->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('item_type', $itemType);
|
||||
|
||||
if (!empty($studentIds)) {
|
||||
$builder->whereIn('student_id', array_map('intval', $studentIds));
|
||||
}
|
||||
|
||||
if ($itemIndexes !== null) {
|
||||
if (!empty($itemIndexes)) {
|
||||
$builder->whereIn('item_index', array_map('intval', $itemIndexes));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} elseif ($nullIndex) {
|
||||
$builder->where('item_index', null);
|
||||
}
|
||||
|
||||
$builder->delete();
|
||||
|
||||
if (empty($checkedItems)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = utc_now();
|
||||
$rows = [];
|
||||
foreach ($checkedItems as $item) {
|
||||
$sid = (int) ($item['student_id'] ?? 0);
|
||||
if ($sid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$rows[] = [
|
||||
'student_id' => $sid,
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'item_type' => $itemType,
|
||||
'item_index' => $item['item_index'] ?? null,
|
||||
'is_allowed' => 1,
|
||||
'updated_by' => $updatedBy,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($rows)) {
|
||||
$this->insertBatch($rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class NavItemModel extends Model
|
||||
{
|
||||
protected $table = 'nav_items';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useTimestamps = true;
|
||||
protected $useSoftDeletes= true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'menu_parent_id','label','url','icon_class','target','sort_order','is_enabled'
|
||||
];
|
||||
|
||||
public function getChildrenOf(int $parentId): array
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
->where('is_enabled', 1)
|
||||
->orderBy('sort_order', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class NotificationModel extends Model
|
||||
{
|
||||
protected $table = 'notifications';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'title',
|
||||
'message',
|
||||
'target_group',
|
||||
'delivery_channels',
|
||||
'priority',
|
||||
'status',
|
||||
'action_url',
|
||||
'attachment_path',
|
||||
'semester',
|
||||
'school_year',
|
||||
'scheduled_at'
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
/**
|
||||
* Get active (non-deleted, non-expired) notifications
|
||||
*/
|
||||
public function getActiveNotifications($targetGroup = null)
|
||||
{
|
||||
$builder = $this->where('scheduled_at <= NOW()')
|
||||
->where('(expires_at IS NULL OR expires_at > NOW())');
|
||||
|
||||
if (!empty($targetGroup)) {
|
||||
$builder->where('target_group', $targetGroup);
|
||||
}
|
||||
|
||||
return $builder->orderBy('priority', 'DESC')
|
||||
->orderBy('scheduled_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get soft-deleted (archived) notifications
|
||||
*/
|
||||
public function getDeletedNotifications()
|
||||
{
|
||||
return $this->onlyDeleted()
|
||||
->orderBy('deleted_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notifications including soft-deleted
|
||||
*/
|
||||
public function getAllNotificationsWithDeleted()
|
||||
{
|
||||
return $this->withDeleted()
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a soft-deleted notification by ID
|
||||
*/
|
||||
public function restoreNotification($id)
|
||||
{
|
||||
return $this->update($id, ['deleted_at' => null]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup expired notifications (optional for cron job use)
|
||||
*/
|
||||
public function deleteExpiredNotifications()
|
||||
{
|
||||
return $this->where('expires_at IS NOT NULL')
|
||||
->where('expires_at < NOW()')
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ParentAttendanceReportModel extends Model
|
||||
{
|
||||
protected $table = 'parent_attendance_reports';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
protected $allowedFields = [
|
||||
'parent_id',
|
||||
'student_id',
|
||||
'class_section_id',
|
||||
'report_date',
|
||||
'type', // absent | late | early_dismissal
|
||||
'arrival_time', // nullable for late
|
||||
'dismiss_time', // nullable for early_dismissal
|
||||
'reason',
|
||||
'semester',
|
||||
'school_year',
|
||||
'status', // new | seen | processed
|
||||
];
|
||||
|
||||
protected $validationRules = [
|
||||
'parent_id' => 'required|is_natural_no_zero',
|
||||
'student_id' => 'required|is_natural_no_zero',
|
||||
'report_date' => 'required|valid_date[Y-m-d]',
|
||||
'type' => 'required|in_list[absent,late,early_dismissal]',
|
||||
'arrival_time' => 'permit_empty|regex_match[/^\d{2}:\d{2}(:\d{2})?$/]',
|
||||
'dismiss_time' => 'permit_empty|regex_match[/^\d{2}:\d{2}(:\d{2})?$/]',
|
||||
'semester' => 'permit_empty|string',
|
||||
'school_year' => 'permit_empty|string',
|
||||
'status' => 'permit_empty|in_list[new,seen,processed]',
|
||||
];
|
||||
|
||||
public function listForDateRange(?string $startDate = null, ?string $endDate = null, ?string $schoolYear = null, ?string $semester = null): array
|
||||
{
|
||||
$b = $this->builder();
|
||||
if ($startDate) {
|
||||
$b->where('report_date >=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$b->where('report_date <=', $endDate);
|
||||
}
|
||||
if (is_string($schoolYear) && $schoolYear !== '') {
|
||||
$b->where('school_year', $schoolYear);
|
||||
}
|
||||
if (is_string($semester) && $semester !== '') {
|
||||
$b->where('semester', $semester);
|
||||
}
|
||||
|
||||
// Join student + class section labels
|
||||
$b->select('parent_attendance_reports.*, s.firstname, s.lastname, cs.class_section_name')
|
||||
->join('students s', 's.id = parent_attendance_reports.student_id', 'left')
|
||||
->join('classSection cs', 'cs.class_section_id = parent_attendance_reports.class_section_id', 'left')
|
||||
->orderBy('report_date', 'ASC')
|
||||
->orderBy('class_section_id', 'ASC')
|
||||
->orderBy('student_id', 'ASC');
|
||||
|
||||
return $b->get()->getResultArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ParentMeetingScheduleModel extends Model
|
||||
{
|
||||
protected $table = 'parent_meeting_schedules';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'parent_user_id',
|
||||
'parent_name',
|
||||
'student_name',
|
||||
'class_section_name',
|
||||
'date',
|
||||
'time',
|
||||
'notes',
|
||||
'semester',
|
||||
'school_year',
|
||||
'status',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ParentModel extends Model
|
||||
{
|
||||
protected $table = 'parents';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'secondparent_firstname',
|
||||
'secondparent_lastname',
|
||||
'secondparent_gender',
|
||||
'secondparent_email',
|
||||
'secondparent_phone',
|
||||
'firstparent_id',
|
||||
'semester',
|
||||
'school_year',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ParentNotificationModel extends Model
|
||||
{
|
||||
protected $table = 'parent_notifications';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
protected $allowedFields = [
|
||||
'student_id','code','incident_date','channel','to_address','subject',
|
||||
'status','response','semester','school_year'
|
||||
];
|
||||
|
||||
public function hasSent(int $studentId, string $code, string $incidentYmd, string $channel='email', ?string $to=null): bool
|
||||
{
|
||||
$qb = $this->where('student_id', $studentId)
|
||||
->where('code', $code)
|
||||
->where('incident_date', $incidentYmd)
|
||||
->where('channel', $channel);
|
||||
if ($to) $qb->where('to_address', $to);
|
||||
|
||||
$row = $qb->orderBy('id','DESC')->first();
|
||||
return $row && ($row['status'] ?? '') === 'sent';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ParticipationModel extends Model
|
||||
{
|
||||
protected $table = 'participation';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
'class_section_id',
|
||||
'updated_by',
|
||||
'score',
|
||||
'comment',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
|
||||
protected array $casts = [];
|
||||
protected array $castHandlers = [];
|
||||
|
||||
// Dates
|
||||
protected $useTimestamps = false;
|
||||
protected $dateFormat = 'datetime';
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
|
||||
// Validation
|
||||
protected $validationRules = [];
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
protected $cleanValidationRules = true;
|
||||
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = [];
|
||||
protected $afterInsert = [];
|
||||
protected $beforeUpdate = [];
|
||||
protected $afterUpdate = [];
|
||||
protected $beforeFind = [];
|
||||
protected $afterFind = [];
|
||||
protected $beforeDelete = [];
|
||||
protected $afterDelete = [];
|
||||
|
||||
// Function to get the final exam score for a specific student, semester, and school year
|
||||
public function getParticipationScore(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): ?float
|
||||
{
|
||||
$builder = $this->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($classSectionId !== null) {
|
||||
$builder->where('class_section_id', $classSectionId);
|
||||
}
|
||||
|
||||
$result = $builder->first();
|
||||
if (!$result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$score = $result['score'] ?? null;
|
||||
if ($score === null || (is_string($score) && trim($score) === '') || $score === '' || !is_numeric($score)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (float) $score;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// app/Models/PasswordResetModel.php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PasswordResetModel extends Model
|
||||
{
|
||||
protected $table = 'password_resets';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'email',
|
||||
'token',
|
||||
'created_at',
|
||||
'expires_at'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'expires_at';
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
// app/Models/PasswordResetRequestModel.php
|
||||
namespace App\Models;
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PasswordResetRequestModel extends Model
|
||||
{
|
||||
protected $table = 'password_reset_requests';
|
||||
protected $allowedFields = ['ip_address', 'requested_at'];
|
||||
public $timestamps = false;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
|
||||
class PayPalPaymentModel extends Model
|
||||
{
|
||||
protected $table = 'paypal_payments';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'webhook_id',
|
||||
'parent_school_id',
|
||||
'order_id',
|
||||
'transaction_id',
|
||||
'status',
|
||||
'amount',
|
||||
'currency',
|
||||
'paypal_fee',
|
||||
'net_amount',
|
||||
'payer_email',
|
||||
'merchant_id',
|
||||
'event_type',
|
||||
'summary',
|
||||
'raw_payload',
|
||||
'synced',
|
||||
'semester',
|
||||
'school_year',
|
||||
'sync_attempts'
|
||||
];
|
||||
|
||||
// Enable timestamps
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = ''; // not used; leave empty if not needed
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PaymentErrorModel extends Model
|
||||
{
|
||||
protected $table = 'payment_error'; // The DB table
|
||||
protected $primaryKey = 'id'; // Primary key
|
||||
|
||||
protected $allowedFields = [
|
||||
'payment_id',
|
||||
'invoice_id',
|
||||
'parent_id',
|
||||
'wrong_paid_amount',
|
||||
'wrong_payment_method',
|
||||
'wrong_check_file',
|
||||
'error_note',
|
||||
'semester',
|
||||
'school_year',
|
||||
'logged_at',
|
||||
'logged_by'
|
||||
];
|
||||
|
||||
protected $useTimestamps = false; // We'll manage logged_at manually
|
||||
protected $validationRules = [
|
||||
'wrong_paid_amount' => 'required|decimal',
|
||||
'payment_id' => 'required|integer',
|
||||
'invoice_id' => 'required|integer',
|
||||
'parent_id' => 'required|integer'
|
||||
];
|
||||
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PaymentModel extends Model
|
||||
{
|
||||
protected $table = 'payments';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
|
||||
protected $allowedFields = [
|
||||
'parent_id',
|
||||
'invoice_id',
|
||||
'total_amount',
|
||||
'paid_amount',
|
||||
'balance',
|
||||
'number_of_installments',
|
||||
'transaction_id',
|
||||
'check_file',
|
||||
'check_number',
|
||||
'payment_method',
|
||||
'payment_date',
|
||||
'school_year',
|
||||
'semester',
|
||||
'status',
|
||||
'updated_by'
|
||||
];
|
||||
|
||||
protected $useTimestamps = false;
|
||||
// ❗ Your DB handles created_at / updated_at with default CURRENT_TIMESTAMP
|
||||
// Remove CI4 auto timestamp management unless you modify your DB schema
|
||||
|
||||
protected $validationRules = [
|
||||
'parent_id' => 'required|integer',
|
||||
'invoice_id' => 'required|integer',
|
||||
'total_amount' => 'required|decimal',
|
||||
'paid_amount' => 'required|decimal',
|
||||
'balance' => 'required|decimal',
|
||||
'number_of_installments' => 'required|integer',
|
||||
'payment_method' => 'required|max_length[50]', // ✅ Removed 'string'
|
||||
'payment_date' => 'required|valid_date[Y-m-d H:i:s]',
|
||||
'status' => 'required|max_length[50]', // ✅ Removed 'string'
|
||||
'check_number' => 'permit_empty|max_length[100]',
|
||||
];
|
||||
|
||||
protected $validationMessages = [
|
||||
'parent_id' => [
|
||||
'required' => 'Parent ID is required.',
|
||||
'integer' => 'Parent ID must be an integer.',
|
||||
],
|
||||
'invoice_id' => [
|
||||
'required' => 'Invoice ID is required.',
|
||||
'integer' => 'Invoice ID must be an integer.',
|
||||
],
|
||||
'total_amount' => [
|
||||
'required' => 'Total amount is required.',
|
||||
'decimal' => 'Total amount must be a valid decimal.',
|
||||
],
|
||||
'paid_amount' => [
|
||||
'required' => 'Paid amount is required.',
|
||||
'decimal' => 'Paid amount must be a valid decimal.',
|
||||
],
|
||||
'balance' => [
|
||||
'required' => 'Balance is required.',
|
||||
'decimal' => 'Balance must be a valid decimal.',
|
||||
],
|
||||
'payment_date' => [
|
||||
'required' => 'Payment date is required.',
|
||||
'valid_date' => 'Payment date must be a valid date (Y-m-d H:i:s).',
|
||||
],
|
||||
'payment_method' => [
|
||||
'required' => 'Payment method is required.',
|
||||
'max_length' => 'Payment method must not exceed 50 characters.',
|
||||
],
|
||||
'status' => [
|
||||
'required' => 'Status is required.',
|
||||
'max_length' => 'Status must not exceed 50 characters.',
|
||||
],
|
||||
'number_of_installments' => [
|
||||
'required' => 'Number of installments is required.',
|
||||
'integer' => 'Number of installments must be an integer.',
|
||||
],
|
||||
'check_number' => [
|
||||
'max_length' => 'Check number must not exceed 100 characters.',
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Get payments by parent ID.
|
||||
*
|
||||
* @param int $parentId
|
||||
* @return array
|
||||
*/
|
||||
public function getPaymentsByParentId(int $parentId): array
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
->orderBy('payment_date', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the total paid amount for a specific parent.
|
||||
*
|
||||
* @param int $parentId
|
||||
* @return float
|
||||
*/
|
||||
public function getTotalPaidByParentId(int $parentId, string $schoolYear): float
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$result = $db->table('payments')
|
||||
->selectSum('paid_amount', 'total_paid')
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
return $result && isset($result['total_paid']) ? (float) $result['total_paid'] : 0.00;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update balance for a specific payment.
|
||||
*
|
||||
* @param int $paymentId
|
||||
* @param float $amountPaid
|
||||
* @return bool
|
||||
*/
|
||||
public function updateBalance(int $paymentId, float $amountPaid): bool
|
||||
{
|
||||
$payment = $this->find($paymentId);
|
||||
if (!$payment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$newBalance = $payment['balance'] - $amountPaid;
|
||||
|
||||
return $this->update($paymentId, [
|
||||
'paid_amount' => $payment['paid_amount'] + $amountPaid,
|
||||
'balance' => $newBalance
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get payments by school year.
|
||||
*
|
||||
* @param string $schoolYear
|
||||
* @return array
|
||||
*/
|
||||
public function getPaymentsByYear(string $schoolYear): array
|
||||
{
|
||||
return $this->where('school_year', $schoolYear)
|
||||
->orderBy('payment_date', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new transaction ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateNewTransactionId(): string
|
||||
{
|
||||
$currentYear = date('Y');
|
||||
$prefix = $currentYear . '-';
|
||||
|
||||
$latest = $this->like('transaction_id', $prefix, 'after')
|
||||
->orderBy('transaction_id', 'DESC')
|
||||
->first();
|
||||
|
||||
if ($latest && isset($latest['transaction_id'])) {
|
||||
$lastNumber = (int) str_replace($prefix, '', $latest['transaction_id']);
|
||||
$newNumber = $lastNumber + 1;
|
||||
} else {
|
||||
$newNumber = 1;
|
||||
}
|
||||
|
||||
return $prefix . str_pad($newNumber, 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
public function getPaymentsByInvoice($invoiceIds): array
|
||||
{
|
||||
// Normalize input
|
||||
if (!is_array($invoiceIds)) {
|
||||
$invoiceIds = [$invoiceIds];
|
||||
}
|
||||
if (empty($invoiceIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Subquery: latest payment_date per invoice (Full/Partial only)
|
||||
$latestSub = $this->db->table('payments')
|
||||
->select('invoice_id, MAX(payment_date) AS max_date')
|
||||
->whereIn('status', ['Full', 'Paid', 'Partially Paid'])
|
||||
->groupBy('invoice_id')
|
||||
->getCompiledSelect();
|
||||
|
||||
// Main query: join payments to the latest per invoice
|
||||
$rows = $this->db->table('payments p')
|
||||
->select('p.invoice_id, p.paid_amount AS last_paid_amount, p.payment_date AS last_payment_date, p.status AS last_payment_status')
|
||||
->join("($latestSub) latest", 'latest.invoice_id = p.invoice_id AND latest.max_date = p.payment_date', 'inner', false) // <-- escape=false
|
||||
->whereIn('p.invoice_id', $invoiceIds)
|
||||
->whereIn('p.status', ['Full', 'Paid', 'Partially Paid'])
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PaymentNotificationLogModel extends Model
|
||||
{
|
||||
protected $table = 'payment_notification_logs';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
protected $allowedFields = [
|
||||
'parent_id',
|
||||
'invoice_id',
|
||||
'school_year',
|
||||
'period_year',
|
||||
'period_month',
|
||||
'type',
|
||||
'to_email',
|
||||
'cc_email',
|
||||
'head_fa_notified',
|
||||
'subject',
|
||||
'body',
|
||||
'status',
|
||||
'error_message',
|
||||
'balance_snapshot',
|
||||
'created_at',
|
||||
'sent_at',
|
||||
];
|
||||
|
||||
protected $useTimestamps = false;
|
||||
|
||||
public function existsForPeriod(int $parentId, int $year, int $month, string $type): bool
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
->where('period_year', $year)
|
||||
->where('period_month', $month)
|
||||
->where('type', $type)
|
||||
->first() !== null;
|
||||
}
|
||||
|
||||
public function listLogs(?string $from = null, ?string $to = null, ?string $type = null): array
|
||||
{
|
||||
$builder = $this->orderBy('sent_at', 'DESC');
|
||||
if ($from) {
|
||||
$builder->where('sent_at >=', $from);
|
||||
}
|
||||
if ($to) {
|
||||
$builder->where('sent_at <=', $to);
|
||||
}
|
||||
if ($type) {
|
||||
$builder->where('type', $type);
|
||||
}
|
||||
return $builder->findAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PaymentTransactionModel extends Model
|
||||
{
|
||||
protected $table = 'payment_transactions'; // Table name
|
||||
protected $primaryKey = 'id'; // Primary key
|
||||
|
||||
// Allowed fields for mass assignment
|
||||
protected $allowedFields = [
|
||||
'transaction_id',
|
||||
'payment_id', // Reference to payments table (the associated payment)
|
||||
'transaction_date',
|
||||
'amount',
|
||||
'payment_method',
|
||||
'payment_status',
|
||||
'transaction_fee',
|
||||
'payment_reference',
|
||||
'semester',
|
||||
'school_year',
|
||||
'is_full_payment', // Flag to track full payment status
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
// Set automatic timestamps
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
// Validation rules
|
||||
protected $validationRules = [
|
||||
'transaction_id' => 'required|is_unique[payment_transactions.transaction_id]',
|
||||
'payment_id' => 'required|integer', // Reference to payment table
|
||||
'amount' => 'required|decimal',
|
||||
'payment_method' => 'required|string|max_length[50]',
|
||||
'payment_status' => 'required|string|max_length[50]',
|
||||
'payment_reference' => 'permit_empty|string|max_length[255]',
|
||||
'is_full_payment' => 'required|in_list[0,1]', // 0 for installment, 1 for full payment
|
||||
];
|
||||
|
||||
// Custom error messages
|
||||
protected $validationMessages = [
|
||||
'transaction_id' => [
|
||||
'required' => 'The transaction ID is required.',
|
||||
'is_unique' => 'The transaction ID must be unique.',
|
||||
],
|
||||
'payment_id' => [
|
||||
'required' => 'The payment ID is required.',
|
||||
'integer' => 'The payment ID must be an integer.',
|
||||
],
|
||||
'amount' => [
|
||||
'required' => 'The payment amount is required.',
|
||||
'decimal' => 'The payment amount must be a valid decimal number.',
|
||||
],
|
||||
'payment_method' => [
|
||||
'required' => 'The payment method is required.',
|
||||
'max_length' => 'The payment method must not exceed 50 characters.',
|
||||
],
|
||||
'payment_status' => [
|
||||
'required' => 'The payment status is required.',
|
||||
'max_length' => 'The payment status must not exceed 50 characters.',
|
||||
],
|
||||
'payment_reference' => [
|
||||
'max_length' => 'The payment reference must not exceed 255 characters.',
|
||||
],
|
||||
'is_full_payment' => [
|
||||
'required' => 'The full payment flag is required.',
|
||||
'in_list' => 'The full payment flag must be either 0 (installment) or 1 (full payment).',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Get all transactions for a specific payment.
|
||||
*
|
||||
* @param int $paymentId
|
||||
* @return array
|
||||
*/
|
||||
public function getTransactionsByPaymentId($paymentId)
|
||||
{
|
||||
return $this->where('payment_id', $paymentId)
|
||||
->orderBy('transaction_date', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single transaction by its transaction ID.
|
||||
*
|
||||
* @param string $transactionId
|
||||
* @return array|null
|
||||
*/
|
||||
public function getTransactionById($transactionId)
|
||||
{
|
||||
return $this->where('transaction_id', $transactionId)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update payment status by transaction ID.
|
||||
*
|
||||
* @param string $transactionId
|
||||
* @param string $status
|
||||
* @return bool
|
||||
*/
|
||||
public function updateTransactionStatus($transactionId, $status)
|
||||
{
|
||||
return $this->update($transactionId, ['payment_status' => $status]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total amount paid for a specific payment.
|
||||
*
|
||||
* @param int $paymentId
|
||||
* @return float
|
||||
*/
|
||||
public function getTotalPaidByPaymentId($paymentId)
|
||||
{
|
||||
$this->selectSum('amount');
|
||||
return $this->where('payment_id', $paymentId)->get()->getRow()->amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update transaction fee for a specific transaction.
|
||||
*
|
||||
* @param string $transactionId
|
||||
* @param float $transactionFee
|
||||
* @return bool
|
||||
*/
|
||||
public function updateTransactionFee($transactionId, $transactionFee)
|
||||
{
|
||||
return $this->update($transactionId, ['transaction_fee' => $transactionFee]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest payment transaction for a payment.
|
||||
*
|
||||
* @param int $paymentId
|
||||
* @return array|null
|
||||
*/
|
||||
public function getLatestTransactionByPaymentId($paymentId)
|
||||
{
|
||||
return $this->where('payment_id', $paymentId)
|
||||
->orderBy('transaction_date', 'DESC')
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PaypalTransactionModel extends Model
|
||||
{
|
||||
protected $table = 'paypal_transactions';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'transaction_id',
|
||||
'payment_id',
|
||||
'firstname',
|
||||
'lastname',
|
||||
'event_type',
|
||||
'payer_email',
|
||||
'amount',
|
||||
'currency',
|
||||
'status',
|
||||
'payment_method',
|
||||
'transaction_fee',
|
||||
'semester',
|
||||
'school_year',
|
||||
'raw_data'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PermissionModel extends Model
|
||||
{
|
||||
protected $table = 'permissions'; // Define the table associated with this model
|
||||
protected $primaryKey = 'id'; // Define the primary key of the table
|
||||
protected $allowedFields = [
|
||||
'name',
|
||||
'description',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
]; // Define the fields that can be set using insert/update methods
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
/**
|
||||
* Fetch all permissions from the database
|
||||
*
|
||||
* @return array List of permissions
|
||||
* @throws \Exception If there is an error during the fetch
|
||||
*/
|
||||
public function getPermissions()
|
||||
{
|
||||
try {
|
||||
// Query to get all permissions from the 'permissions' table
|
||||
return $this->findAll();
|
||||
} catch (\Exception $e) {
|
||||
// Log the error message if the query fails
|
||||
log_message('error', 'Error fetching permissions: ' . $e->getMessage());
|
||||
// Rethrow the exception to be handled by the caller
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch permissions assigned to a specific role
|
||||
*
|
||||
* @param int $role_id The ID of the role
|
||||
* @return array List of permissions associated with the role
|
||||
* @throws \Exception If there is an error during the fetch
|
||||
*/
|
||||
public function getRolePermissions($role_id)
|
||||
{
|
||||
try {
|
||||
// Query to get role permissions along with default permissions
|
||||
$rolePermissions = $this->db->table('role_permissions')
|
||||
->select('role_permissions.*, permissions.name')
|
||||
// Join with 'permissions' table to get permission details
|
||||
->join('permissions', 'role_permissions.permission_id = permissions.id', 'left')
|
||||
// Filter by the specified role ID
|
||||
->where('role_permissions.role_id', $role_id)
|
||||
->get()
|
||||
// Get the result as an array
|
||||
->getResultArray();
|
||||
// Log the fetched role permissions for debugging
|
||||
log_message('info', 'Role permissions fetched: ' . print_r($rolePermissions, true));
|
||||
return $rolePermissions;
|
||||
} catch (\Exception $e) {
|
||||
// Log the error message if the query fails
|
||||
log_message('error', 'Error fetching role permissions for role ID: ' . $role_id . ': ' . $e->getMessage());
|
||||
// Rethrow the exception to be handled by the caller
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PlacementBatchModel extends Model
|
||||
{
|
||||
protected $table = 'placement_batches';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'placement_test',
|
||||
'school_year',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PlacementLevelModel extends Model
|
||||
{
|
||||
protected $table = 'placement_levels';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'level',
|
||||
'school_year',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PlacementScoreModel extends Model
|
||||
{
|
||||
protected $table = 'placement_scores';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'batch_id',
|
||||
'student_id',
|
||||
'score',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PreferencesModel extends Model
|
||||
{
|
||||
protected $table = 'user_preferences'; // Adjusted to match the table name
|
||||
protected $primaryKey = 'id'; // Primary key of the table
|
||||
|
||||
// Allowed fields to enable mass assignment
|
||||
protected $allowedFields = [
|
||||
'user_id', // Foreign key linking preferences to a user
|
||||
'receive_email_notifications', // Email notifications preference
|
||||
'receive_sms_notifications', // SMS notifications preference
|
||||
'theme', // Theme preference (e.g., light or dark)
|
||||
'language', // Language preference
|
||||
'timezone', // Timezone preference
|
||||
'style_color', // UI accent color key
|
||||
'menu_color', // Menu palette key or 'custom'
|
||||
'menu_custom_bg', // Custom menu background color
|
||||
'menu_custom_text', // Custom menu text color
|
||||
'menu_custom_mode', // Custom menu mode: light|dark
|
||||
'receive_push_notifications', // Push notifications preference
|
||||
'daily_summary_email', // Daily summary email preference
|
||||
'privacy_mode', // Privacy mode setting
|
||||
'marketing_emails', // Marketing emails preference
|
||||
'account_activity_alerts', // Account activity alerts preference
|
||||
'created_at', // Timestamp of when the record was created
|
||||
'updated_at' // Timestamp of when the record was last updated
|
||||
];
|
||||
|
||||
// Enable automatic timestamp management if needed
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PrintRequestModel extends Model
|
||||
{
|
||||
protected $table = 'print_requests';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $allowedFields = [
|
||||
'teacher_id',
|
||||
'admin_id',
|
||||
'class_id',
|
||||
'file_path',
|
||||
'page_selection',
|
||||
'num_copies',
|
||||
'required_by',
|
||||
'pickup_method',
|
||||
'status',
|
||||
];
|
||||
|
||||
// Dates
|
||||
protected $useTimestamps = true;
|
||||
protected $dateFormat = 'datetime';
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
// Validation
|
||||
protected $validationRules = [];
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
protected $cleanValidationRules = true;
|
||||
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = [];
|
||||
protected $afterInsert = [];
|
||||
protected $beforeUpdate = [];
|
||||
protected $afterUpdate = [];
|
||||
protected $beforeFind = [];
|
||||
protected $afterFind = [];
|
||||
protected $beforeDelete = [];
|
||||
protected $afterDelete = [];
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ProjectModel extends Model
|
||||
{
|
||||
protected $table = 'project';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
'class_section_id',
|
||||
'updated_by',
|
||||
'project_index',
|
||||
'score',
|
||||
'comment',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* Calculate the average project score for a given student, semester, and school year.
|
||||
*
|
||||
* @param int $studentId The student's ID.
|
||||
* @param string $semester The semester (e.g., 'Fall', 'Spring').
|
||||
* @param string $schoolYear The school year (e.g., '2024-2025').
|
||||
* @param int|null $classSectionId Optional class section filter.
|
||||
* @return float|null The average project score, or null if no scores are found.
|
||||
*/
|
||||
public function getAverageProjectScore(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): ?float
|
||||
{
|
||||
// Fetch the project scores for the given student, semester, and school year
|
||||
$builder = $this->builder();
|
||||
$builder->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($classSectionId !== null) {
|
||||
$builder->where('class_section_id', $classSectionId);
|
||||
}
|
||||
|
||||
// Execute the query and get the results
|
||||
$query = $builder->get();
|
||||
$results = $query->getResultArray();
|
||||
|
||||
// If there are no scores, return null
|
||||
if (empty($results)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate the average score (ignore blank/null/non-numeric)
|
||||
$totalScore = 0.0;
|
||||
$scoreCount = 0;
|
||||
|
||||
foreach ($results as $result) {
|
||||
$score = $result['score'] ?? null;
|
||||
if ($score === null || (is_string($score) && trim($score) === '') || $score === '' || !is_numeric($score)) {
|
||||
continue;
|
||||
}
|
||||
$totalScore += (float) $score;
|
||||
$scoreCount++;
|
||||
}
|
||||
|
||||
if ($scoreCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $totalScore / $scoreCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PromotionQueueModel extends Model
|
||||
{
|
||||
protected $table = 'promotion_queue';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'from_class_section_id',
|
||||
'to_class_id',
|
||||
'to_class_section_id',
|
||||
'school_year_from',
|
||||
'school_year_to',
|
||||
'status',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
public function upsert(array $data): bool
|
||||
{
|
||||
if (empty($data['student_id']) || empty($data['school_year_to'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$existing = $this->where('student_id', (int)$data['student_id'])
|
||||
->where('school_year_to', (string)$data['school_year_to'])
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return (bool) $this->update((int)$existing[$this->primaryKey], array_merge($existing, $data));
|
||||
}
|
||||
|
||||
return (bool) $this->insert($data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PurchaseOrderItemModel extends Model
|
||||
{
|
||||
protected $table = 'purchase_order_items';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = ['purchase_order_id','supply_id','description','quantity','received_qty','unit_cost'];
|
||||
protected $useTimestamps = true;
|
||||
|
||||
protected $validationRules = [
|
||||
'purchase_order_id' => 'required|is_natural_no_zero',
|
||||
'supply_id' => 'required|is_natural_no_zero',
|
||||
'quantity' => 'required|is_natural_no_zero',
|
||||
'unit_cost' => 'required|decimal',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PurchaseOrderModel extends Model
|
||||
{
|
||||
protected $table = 'purchase_orders';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = ['po_number','supplier_id','status','order_date','expected_date','subtotal','tax','total','notes'];
|
||||
protected $useTimestamps = true;
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
protected $validationRules = [
|
||||
'po_number' => 'required|max_length[60]|is_unique[purchase_orders.po_number,id,{id}]',
|
||||
'supplier_id' => 'required|is_natural_no_zero',
|
||||
'status' => 'in_list[draft,ordered,received,canceled]',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class QuizModel extends Model
|
||||
{
|
||||
protected $table = 'quiz';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
'class_section_id',
|
||||
'updated_by',
|
||||
'quiz_index',
|
||||
'score',
|
||||
'comment',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
|
||||
protected array $casts = [];
|
||||
protected array $castHandlers = [];
|
||||
|
||||
// Dates
|
||||
protected $useTimestamps = false;
|
||||
protected $dateFormat = 'datetime';
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
|
||||
// Validation
|
||||
protected $validationRules = [];
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
protected $cleanValidationRules = true;
|
||||
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = [];
|
||||
protected $afterInsert = [];
|
||||
protected $beforeUpdate = [];
|
||||
protected $afterUpdate = [];
|
||||
protected $beforeFind = [];
|
||||
protected $afterFind = [];
|
||||
protected $beforeDelete = [];
|
||||
protected $afterDelete = [];
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the average quiz score for a given student, semester, and school year.
|
||||
*
|
||||
* @param int $studentId The student's ID.
|
||||
* @param string $semester The semester (e.g., 'Fall', 'Spring').
|
||||
* @param string $schoolYear The school year (e.g., '2024-2025').
|
||||
* @param int|null $classSectionId Optional class section filter.
|
||||
* @return float|null The average quiz score, or null if no scores are found.
|
||||
*/
|
||||
public function getAverageQuizScore(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): ?float
|
||||
{
|
||||
// Fetch the quiz scores for the given student, semester, and school year
|
||||
$builder = $this->builder();
|
||||
$builder->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($classSectionId !== null) {
|
||||
$builder->where('class_section_id', $classSectionId);
|
||||
}
|
||||
|
||||
// Execute the query and get the results
|
||||
$query = $builder->get();
|
||||
$results = $query->getResultArray();
|
||||
|
||||
// If there are no scores, return null
|
||||
if (empty($results)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate the average score (ignore blank/null/non-numeric)
|
||||
$totalScore = 0.0;
|
||||
$scoreCount = 0;
|
||||
|
||||
foreach ($results as $result) {
|
||||
$score = $result['score'] ?? null;
|
||||
if ($score === null || (is_string($score) && trim($score) === '') || $score === '' || !is_numeric($score)) {
|
||||
continue;
|
||||
}
|
||||
$totalScore += (float) $score;
|
||||
$scoreCount++;
|
||||
}
|
||||
|
||||
if ($scoreCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $totalScore / $scoreCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class RefundModel extends Model
|
||||
{
|
||||
protected $table = 'refunds';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'parent_id',
|
||||
'school_year',
|
||||
'invoice_id',
|
||||
'refund_amount',
|
||||
'requested_at',
|
||||
'approved_at',
|
||||
'refunded_at',
|
||||
'status',
|
||||
'reason',
|
||||
'refund_paid_amount',
|
||||
'request',
|
||||
'note',
|
||||
'semester',
|
||||
'school_year',
|
||||
'approved_by',
|
||||
'updated_by',
|
||||
'refund_method',
|
||||
'check_nbr',
|
||||
'check_file'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
/**
|
||||
* Get total approved refund for a parent in a specific school year.
|
||||
*
|
||||
* @param int $parentId
|
||||
* @param string $schoolYear
|
||||
* @return float
|
||||
*/
|
||||
public function getTotalApprovedRefundByParentIdAndSchoolYear(int $parentId, string $schoolYear): float
|
||||
{
|
||||
// Sum refunds that have been at least partially paid out to the parent
|
||||
$result = $this->selectSum('refund_paid_amount', 'total_paid')
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('status', ['Partial', 'Paid'])
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
return $result && isset($result['total_paid']) ? (float) $result['total_paid'] : 0.00;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ReimbursementBatchAdminFileModel extends Model
|
||||
{
|
||||
protected $table = 'reimbursement_batch_admin_files';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'batch_id',
|
||||
'admin_id',
|
||||
'filename',
|
||||
'original_filename',
|
||||
'uploaded_at',
|
||||
'uploaded_by',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ReimbursementBatchItemModel extends Model
|
||||
{
|
||||
protected $table = 'reimbursement_batch_items';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'batch_id',
|
||||
'expense_id',
|
||||
'reimbursement_id',
|
||||
'admin_id',
|
||||
'assigned_at',
|
||||
'unassigned_at',
|
||||
'notes',
|
||||
'school_year',
|
||||
'semester',
|
||||
];
|
||||
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ReimbursementBatchModel extends Model
|
||||
{
|
||||
protected $table = 'reimbursement_batches';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'title',
|
||||
'status',
|
||||
'created_by',
|
||||
'closed_by',
|
||||
'opened_at',
|
||||
'closed_at',
|
||||
'notes',
|
||||
'school_year',
|
||||
'semester',
|
||||
'yearly_batch_number',
|
||||
];
|
||||
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ReimbursementModel extends Model
|
||||
{
|
||||
protected $table = 'reimbursements';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'amount',
|
||||
'reimbursed_to',
|
||||
'approved_by',
|
||||
'receipt_path',
|
||||
'description',
|
||||
'status',
|
||||
'expense_id',
|
||||
'added_by',
|
||||
'school_year',
|
||||
'semester',
|
||||
'check_number',
|
||||
'reimbursement_method',
|
||||
'batch_number'
|
||||
];
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class RoleModel extends Model
|
||||
{
|
||||
protected $table = 'roles';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
'dashboard_route',
|
||||
'priority',
|
||||
'is_active',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
public function findByNamesOrSlugs(array $keys): array
|
||||
{
|
||||
if (empty($keys)) return [];
|
||||
|
||||
// Normalize for case-insensitive matching
|
||||
$lower = array_map('strtolower', $keys);
|
||||
|
||||
// We’ll match on lower(name) or lower(slug)
|
||||
return $this->where('is_active', 1)
|
||||
->groupStart()
|
||||
->whereIn('LOWER(name)', $lower)
|
||||
->orWhereIn('LOWER(slug)', $lower)
|
||||
->groupEnd()
|
||||
->orderBy('priority', 'ASC') // highest priority first
|
||||
->findAll();
|
||||
}
|
||||
|
||||
public function getRouteByNameOrSlug(string $key): ?string
|
||||
{
|
||||
$row = $this->where('is_active', 1)
|
||||
->groupStart()
|
||||
->where('LOWER(name)', strtolower($key))
|
||||
->orWhere('LOWER(slug)', strtolower($key))
|
||||
->groupEnd()
|
||||
->first();
|
||||
|
||||
return $row['dashboard_route'] ?? null;
|
||||
}
|
||||
|
||||
public function getRoles()
|
||||
{
|
||||
return $this->findAll();
|
||||
}
|
||||
|
||||
/** Map role names -> role IDs */
|
||||
public function getIdsByNames(array $names): array
|
||||
{
|
||||
$names = array_values(array_filter(array_map('strval', $names)));
|
||||
if (empty($names)) return [];
|
||||
|
||||
// collation is usually case-insensitive; if not, add LOWER() both sides
|
||||
$ids = $this->select('id')->whereIn('name', $names)->findColumn('id');
|
||||
return array_map('intval', $ids ?? []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class RoleNavItemModel extends Model
|
||||
{
|
||||
protected $table = 'role_nav_items';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useTimestamps = true;
|
||||
|
||||
// ✅ columns that actually exist now
|
||||
protected $allowedFields = ['role_id','nav_item_id'];
|
||||
|
||||
/**
|
||||
* Accepts role names (strings from session), resolves to role IDs, then returns allowed nav_item_ids.
|
||||
*/
|
||||
public function getNavItemIdsForRoles(array $roleNames): array
|
||||
{
|
||||
$roleNames = array_values(array_filter(array_map('strval', $roleNames)));
|
||||
if (empty($roleNames)) return [];
|
||||
|
||||
$roleModel = new RoleModel();
|
||||
$roleIds = $roleModel->getIdsByNames($roleNames);
|
||||
if (empty($roleIds)) return [];
|
||||
|
||||
$rows = $this->select('nav_item_id')
|
||||
->whereIn('role_id', $roleIds)
|
||||
->findAll();
|
||||
|
||||
return array_values(array_unique(array_column($rows, 'nav_item_id')));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class RolePermissionModel extends Model
|
||||
{
|
||||
protected $table = 'role_permissions'; // Define the table associated with this model
|
||||
protected $primaryKey = 'id'; // Define the primary key of the table
|
||||
protected $allowedFields = [
|
||||
'role_id',
|
||||
'permission_id',
|
||||
'can_create',
|
||||
'can_read',
|
||||
'can_update',
|
||||
'can_delete',
|
||||
'can_manage',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
]; // Define the fields that can be set using insert/update methods
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
|
||||
/**
|
||||
* Fetch all role permissions with optional limit and offset
|
||||
*
|
||||
* @param int $limit Number of records to return
|
||||
* @param int $offset Number of records to skip
|
||||
* @return array List of role permissions
|
||||
* @throws \Exception If there is an error during the fetch
|
||||
*/
|
||||
/**
|
||||
* Fetch permissions assigned to a specific role
|
||||
*
|
||||
* @param int $role_id The ID of the role
|
||||
* @return array List of permissions associated with the role
|
||||
* @throws \Exception If there is an error during the fetch
|
||||
*/
|
||||
public function getRolePermissions($role_id)
|
||||
{
|
||||
try {
|
||||
// Query to get role permissions based on role ID
|
||||
return $this->where('role_id', $role_id)->findAll();
|
||||
} catch (\Exception $e) {
|
||||
// Log the error message if the query fails
|
||||
log_message('error', 'Error fetching role permissions for role ID: ' . $role_id . ': ' . $e->getMessage());
|
||||
// Rethrow the exception to be handled by the caller
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function getPermissionsByRole($roleId)
|
||||
{
|
||||
return $this->where('role_id', $roleId)->findAll();
|
||||
}
|
||||
|
||||
public function deleteByRole($roleId)
|
||||
{
|
||||
return $this->where('role_id', $roleId)->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ScoreCommentModel extends Model
|
||||
{
|
||||
protected $table = 'score_comments';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'class_section_id',
|
||||
'score_type',
|
||||
'semester',
|
||||
'school_year',
|
||||
'comment',
|
||||
'comment_review',
|
||||
'reviewed_by',
|
||||
'commented_by',
|
||||
'created_at'
|
||||
];
|
||||
|
||||
public $timestamps = false; // because we're using MySQL's default timestamp
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class SectionModel extends Model
|
||||
{
|
||||
protected $table = 'sections';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $allowedFields = [
|
||||
'section_name',
|
||||
'description',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'updated_by'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
/**
|
||||
* Get section by name.
|
||||
*
|
||||
* @param string $sectionName
|
||||
* @return array|null
|
||||
*/
|
||||
public function getSectionByName($sectionName)
|
||||
{
|
||||
return $this->where('section_name', $sectionName)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update section details by ID.
|
||||
*
|
||||
* @param int $sectionId
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function updateSection($sectionId, $data)
|
||||
{
|
||||
$data['updated_at'] = utc_now(); // Set the updated_at timestamp
|
||||
return $this->update($sectionId, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete section by ID.
|
||||
*
|
||||
* @param int $sectionId
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteSection($sectionId)
|
||||
{
|
||||
return $this->delete($sectionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class SemesterScoreModel extends Model
|
||||
{
|
||||
protected $table = 'semester_scores';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
'class_section_id',
|
||||
'updated_by',
|
||||
'homework_avg',
|
||||
'quiz_avg',
|
||||
'project_avg',
|
||||
'midterm_exam_score',
|
||||
'final_exam_score',
|
||||
'attendance_score',
|
||||
'participation_score',
|
||||
'ptap_score',
|
||||
'test_avg',
|
||||
'semester_score',
|
||||
'semester',
|
||||
'school_year'
|
||||
];
|
||||
|
||||
/**
|
||||
* Insert or update a semester score record
|
||||
*/
|
||||
// In SemesterScoreModel.php
|
||||
|
||||
public function upsert(array $data): bool
|
||||
{
|
||||
// First check if record exists
|
||||
$existing = $this->where([
|
||||
'student_id' => $data['student_id'],
|
||||
'semester' => $data['semester'],
|
||||
'school_year' => $data['school_year']
|
||||
])->first();
|
||||
|
||||
// If exists - UPDATE
|
||||
if ($existing) {
|
||||
return $this->update($existing['id'], $data);
|
||||
}
|
||||
// If new - INSERT
|
||||
else {
|
||||
return $this->insert($data) !== false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class SettingsModel extends Model
|
||||
{
|
||||
protected $table = 'settings';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'name',
|
||||
'timezone',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
public function getSettings()
|
||||
{
|
||||
return $this->findAll()[0]; // Assuming there's only one settings record
|
||||
}
|
||||
|
||||
public function updateSettings($data)
|
||||
{
|
||||
return $this->update(1, $data); // Assuming there's only one settings record
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,506 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class StaffModel extends Model
|
||||
{
|
||||
protected $table = 'staff';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false; // We're handling 'inactive' via active_role column
|
||||
|
||||
protected $allowedFields = [
|
||||
'user_id',
|
||||
'firstname',
|
||||
'lastname',
|
||||
'email',
|
||||
'phone',
|
||||
'role_name',
|
||||
'active_role',
|
||||
'status',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
protected $useTimestamps = false; // Managed manually in controller
|
||||
|
||||
public function getActiveStaff()
|
||||
{
|
||||
return $this->where('active_role !=', 'inactive')
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
public function getInactiveStaff()
|
||||
{
|
||||
return $this->where('active_role', 'inactive')
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update a staff row keyed by (user_id, school_year).
|
||||
* Ensures created_at is only set on insert and updated_at can be provided by caller.
|
||||
*/
|
||||
public function upsert(array $data): bool
|
||||
{
|
||||
if (!isset($data['user_id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If school_year not provided, try to keep existing or fall back to current year string
|
||||
$schoolYear = $data['school_year'] ?? null;
|
||||
|
||||
if ($schoolYear === null) {
|
||||
// Try to find any existing row by user_id to reuse its school_year
|
||||
$existingAny = $this->where('user_id', $data['user_id'])->orderBy('id', 'DESC')->first();
|
||||
if ($existingAny && isset($existingAny['school_year'])) {
|
||||
$schoolYear = $existingAny['school_year'];
|
||||
$data['school_year'] = $schoolYear;
|
||||
}
|
||||
}
|
||||
|
||||
$existing = null;
|
||||
if ($schoolYear !== null) {
|
||||
$existing = $this->where('user_id', $data['user_id'])
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
} else {
|
||||
// If no school_year, treat (user_id) as key
|
||||
$existing = $this->where('user_id', $data['user_id'])->first();
|
||||
}
|
||||
|
||||
$now = utc_now();
|
||||
|
||||
if ($existing) {
|
||||
// Preserve original created_at unless explicitly provided
|
||||
if (!isset($data['created_at']) && isset($existing['created_at'])) {
|
||||
$data['created_at'] = $existing['created_at'];
|
||||
}
|
||||
// Ensure updated_at exists
|
||||
if (!isset($data['updated_at'])) {
|
||||
$data['updated_at'] = $now;
|
||||
}
|
||||
return $this->update($existing['id'], $data);
|
||||
}
|
||||
|
||||
// New row — set created_at/updated_at if not provided
|
||||
if (!isset($data['created_at'])) {
|
||||
$data['created_at'] = $now;
|
||||
}
|
||||
if (!isset($data['updated_at'])) {
|
||||
$data['updated_at'] = $now;
|
||||
}
|
||||
|
||||
return $this->insert($data) !== false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class StatsModel extends Model
|
||||
{
|
||||
protected $table = 'stats';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'students',
|
||||
'teachers',
|
||||
'admins',
|
||||
'users'
|
||||
];
|
||||
|
||||
public function getStats()
|
||||
{
|
||||
return $this->findAll();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user