add projet

This commit is contained in:
root
2026-03-05 12:29:37 -05:00
parent 8d1eef8ba8
commit 23b7db1107
9109 changed files with 1106501 additions and 73 deletions
+161
View File
@@ -0,0 +1,161 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use App\Models\Invoice;
class AdditionalCharge extends Model
{
protected $table = 'additional_charges';
protected $primaryKey = 'id';
public $timestamps = true;
protected $fillable = [
'parent_id',
'invoice_id',
'school_year',
'semester',
'charge_type',
'title',
'description',
'amount',
'due_date',
'status',
'created_by',
'created_at',
'updated_at',
];
protected $casts = [
'parent_id' => 'integer',
'invoice_id' => 'integer',
'amount' => 'decimal:2',
'created_by' => 'integer',
'due_date' => 'date',
];
/*
|--------------------------------------------------------------------------
| Relationships
|--------------------------------------------------------------------------
*/
public function parent()
{
return $this->belongsTo(User::class, 'parent_id');
}
public function invoice()
{
return $this->belongsTo(Invoice::class, 'invoice_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
/*
|--------------------------------------------------------------------------
| Custom Query Scopes (CI equivalent of byParentTerm)
|--------------------------------------------------------------------------
*/
public function scopeByParentTerm(Builder $query, int $parentId, string $year, string $semester, ?string $status = null)
{
$query->where('parent_id', $parentId)
->where('school_year', $year)
->where('semester', $semester);
if ($status !== null) {
$query->where('status', $status);
}
return $query->orderBy('id', 'DESC');
}
/*
|--------------------------------------------------------------------------
| Custom Methods
|--------------------------------------------------------------------------
*/
public function markApplied($ids, int $invoiceId): bool
{
$ids = is_array($ids) ? $ids : [$ids];
if (empty($ids)) {
return true;
}
$ids = array_map('intval', $ids);
return $this->whereIn('id', $ids)
->update([
'status' => 'applied',
'invoice_id' => $invoiceId
]) >= 0;
}
/*
|--------------------------------------------------------------------------
| Pagination Search (CI paginate alternative)
|--------------------------------------------------------------------------
*/
public function listAllForTerm(
string $schoolYear,
string $semester,
?string $status = null,
?string $q = null,
int $perPage = 50,
bool $fallbackToYear = true
)
{
$build = function (?string $semesterFilter) use ($schoolYear, $status, $q) {
$query = self::query()
->select('additional_charges.*')
->selectRaw("CONCAT(COALESCE(users.lastname,''), ', ', COALESCE(users.firstname,'')) AS parent_name")
->selectRaw('invoices.invoice_number')
->leftJoin('users', 'users.id', '=', 'additional_charges.parent_id')
->leftJoin('invoices', 'invoices.id', '=', 'additional_charges.invoice_id')
->where('additional_charges.school_year', $schoolYear);
if ($semesterFilter !== null && $semesterFilter !== '') {
$query->where('additional_charges.semester', $semesterFilter);
}
if (!empty($status)) {
$query->where('additional_charges.status', $status);
}
if (!empty($q)) {
$query->where(function ($sub) use ($q) {
$sub->where('additional_charges.title', 'like', "%$q%")
->orWhere('additional_charges.description', 'like', "%$q%")
->orWhere('users.firstname', 'like', "%$q%")
->orWhere('users.lastname', 'like', "%$q%")
->orWhere('invoices.invoice_number', 'like', "%$q%");
});
}
return $query;
};
$result = $build($semester)
->orderBy('additional_charges.id', 'DESC')
->paginate($perPage);
if ($fallbackToYear && $result->count() === 0 && $semester !== '') {
$result = $build(null)
->orderBy('additional_charges.id', 'DESC')
->paginate($perPage);
}
return $result;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class AdminNotificationSubject extends BaseModel
{
protected $table = 'admin_notification_subjects';
protected $primaryKey = 'id';
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
protected $fillable = [
'admin_id',
'subject',
'created_at',
'updated_at',
];
public function admin()
{
return $this->belongsTo(User::class, 'admin_id');
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models;
class Assignment extends BaseModel
{
protected $table = 'assignments';
protected $primaryKey = 'id';
public $timestamps = false;
protected $fillable = [
'title',
'description',
'due_date',
'class_id',
'teacher_id',
'student_id',
];
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class AttendanceCommentTemplate extends BaseModel
{
protected $table = 'attendance_comment_template';
protected $primaryKey = 'id';
public $timestamps = true;
protected $fillable = [
'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();
}
}
+461
View File
@@ -0,0 +1,461 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class AttendanceData extends Model
{
protected $table = 'attendance_data';
protected $primaryKey = 'id';
public $timestamps = true;
protected $fillable = [
'class_id',
'class_section_id',
'student_id',
'school_id',
'date',
'status',
'reason',
'is_reported',
'is_notified',
'semester',
'school_year',
'modified_by',
'created_at',
'updated_at',
];
protected $casts = [
'class_id' => 'integer',
'class_section_id' => 'integer',
'school_id' => 'integer',
'student_id' => 'integer',
'reported' => 'string',
'is_reported' => 'string',
'is_notified' => 'string',
'date' => 'date',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
private ?array $reportedColumns = null;
/*
|--------------------------------------------------------------------------
| Relationships
|--------------------------------------------------------------------------
*/
public function student()
{
return $this->belongsTo(Student::class, 'student_id');
}
public function class()
{
return $this->belongsTo(ClassModel::class, 'class_id');
}
public function section()
{
return $this->belongsTo(ClassSection::class, 'class_section_id');
}
/*
|--------------------------------------------------------------------------
| Methods Converted from CI4
|--------------------------------------------------------------------------
*/
/** CI => getAttendanceByClass */
public function getAttendanceByClass(int $classId, int $classSectionId, string $date)
{
return self::where('class_id', $classId)
->where('class_section_id', $classSectionId)
->where('date', $date)
->get()
->toArray();
}
/** CI => unreportedTermRows */
public function unreportedTermRows(array $studentIds, string $schoolYear, ?string $semester): array
{
$qb = DB::table($this->table)
->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()
->map(static function ($row) {
return (array) $row;
})
->all();
}
/** CI => updateAttendance */
public function updateAttendance(int $id, array $data)
{
return self::where('id', $id)->update($data) > 0;
}
/** CI => getAttendance */
public function getAttendance(int $studentId, int $classSectionId, string $date)
{
$row = self::where([
'student_id' => $studentId,
'class_section_id' => $classSectionId,
'date' => $date
])->first();
return $row ? $row->toArray() : null;
}
/** CI => getReportedAttendance */
public function getReportedAttendance(string $startDate, ?string $endDate = null)
{
$query = self::where(function ($q) {
$q->where('is_reported', 1)
->orWhere('is_reported', 'yes');
});
if ($endDate) {
$query->whereBetween('date', [$startDate, $endDate]);
} else {
$query->where('date', $startDate);
}
return $query->get()->toArray();
}
/** CI => getTodayAbsentees */
public function getTodayAbsentees(?int $classSectionId = null, ?string $schoolYear = null, ?string $semester = null)
{
$query = self::where('status', 'Absent')
->where('date', now()->toDateString());
if ($classSectionId !== null) {
$query->where('class_section_id', $classSectionId);
}
if ($schoolYear !== null) {
$query->where('school_year', $schoolYear);
}
if ($semester !== null) {
$query->where('semester', $semester);
}
return $query->get()->toArray();
}
/** CI => getTodayLates */
public function getTodayLates(?int $classSectionId = null, ?string $schoolYear = null, ?string $semester = null)
{
$query = self::where('status', 'Late')
->where('date', now()->toDateString());
if ($classSectionId !== null) {
$query->where('class_section_id', $classSectionId);
}
if ($schoolYear !== null) {
$query->where('school_year', $schoolYear);
}
if ($semester !== null) {
$query->where('semester', $semester);
}
return $query->get()->toArray();
}
/** CI => getAbsencesByClassSection */
public function getAbsencesByClassSection($classSectionId, $schoolYear, $semester)
{
return self::where('status', 'absent')
->where('class_section_id', $classSectionId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->orderBy('date', 'DESC')
->get()
->toArray();
}
/** CI => getStudentAttendance */
public function getStudentAttendance(
int $studentId,
?string $startDate = null,
?string $endDate = null,
?string $schoolYear = null,
?string $semester = null
): array {
$query = self::where('student_id', $studentId);
if ($startDate) {
$query->where('date', '>=', $startDate);
}
if ($endDate) {
$query->where('date', '<=', $endDate);
}
if ($schoolYear) {
$query->where('school_year', $schoolYear);
}
if ($semester) {
$query->where('semester', $semester);
}
return $query->orderBy('date', 'DESC')->get()->toArray();
}
/** CI => getStudentAttendanceOnDate */
public function getStudentAttendanceOnDate(int $studentId, string $date): ?array
{
$row = self::where('student_id', $studentId)
->where('date', $date)
->first();
return $row ? $row->toArray() : null;
}
/*
|--------------------------------------------------------------------------
| CI Complex Function => getUnreportedAbsencesAndLates()
|--------------------------------------------------------------------------
*/
public function getUnreportedAbsencesAndLates(?string $schoolYear = null, ?string $semester = null): array
{
$query = DB::table($this->table)
->select('id', 'student_id', 'class_id', 'class_section_id', 'date', 'status', 'reason', 'school_year', 'semester', 'is_reported')
->whereNotNull('student_id');
$this->applyUnreportedFilter($query);
$query->where(function ($q) {
$q->where('is_notified', 0)
->orWhere('is_notified', '0')
->orWhereNull('is_notified')
->orWhereRaw("LOWER(TRIM(COALESCE(is_notified, ''))) != 'yes'");
});
if ($schoolYear) {
$query->where('school_year', $schoolYear);
}
if ($semester) {
$query->where('semester', $semester);
}
// Status filter
$query->where(function ($q) {
$q->whereIn('status', ['ABS', 'ABSENT', 'LATE', 'A', 'L'])
->orWhere('status', 'like', 'ABS%')
->orWhere('status', 'like', 'LATE%');
});
$rows = $query->orderBy('student_id', 'ASC')
->orderBy('date', 'DESC')
->get()
->toArray();
// Now group output like original CI function
$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],
];
}
$status = strtoupper($r->status);
$isAbs = $status === 'ABS' || $status === 'ABSENT' || $status === 'A' || str_starts_with($status, 'ABS');
$isLate = $status === 'LATE' || $status === 'L' || str_starts_with($status, '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;
}
/*
|--------------------------------------------------------------------------
| CI => markRuleAsNotifiedData()
|--------------------------------------------------------------------------
*/
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';
$query = DB::table($this->table)
->where('student_id', $studentId)
->where('date >=', $start)
->where('date <', $endEx);
if (!empty($semester)) {
$query->where('semester', $semester);
}
if (!empty($schoolYear)) {
$query->where('school_year', $schoolYear);
}
return $query->update([
'is_notified' => 'yes',
'updated_at' => utc_now(),
]) > 0;
}
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 = DB::table($this->table)
->where('student_id', $studentId)
->whereIn('date', $dates)
->whereRaw("LOWER(TRIM(status)) IN ('absent','late')");
if (!empty($semester)) {
$builder->where('semester', $semester);
}
if (!empty($schoolYear)) {
$builder->where('school_year', $schoolYear);
}
$cols = $this->reportedColumns();
$updates = [
'updated_at' => utc_now(),
];
if (!empty($cols['is_reported'])) {
$updates['is_reported'] = 'yes';
}
if (!empty($cols['reported'])) {
$updates['reported'] = 1;
}
if (Schema::hasColumn($this->table, 'is_notified')) {
$updates['is_notified'] = 'yes';
}
return $builder->update($updates) > 0;
}
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'));
$placeholders = implode(',', array_fill(0, count($statuses), '?'));
$qb = DB::table($this->table)
->select('date')
->where('student_id', $studentId)
->where('date >=', $start)
->where('date <=', $day)
->whereRaw("LOWER(TRIM(status)) IN ({$placeholders})", $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()->toArray();
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' => Schema::hasColumn($this->table, 'is_reported'),
'reported' => Schema::hasColumn($this->table, 'reported'),
];
return $this->reportedColumns;
}
private function applyUnreportedFilter($qb): void
{
$cols = $this->reportedColumns();
if (!empty($cols['is_reported'])) {
$qb->where(function ($q) {
$q->where('is_reported', 'no')
->orWhere('is_reported', 0)
->orWhere('is_reported', '0')
->orWhereNull('is_reported');
});
}
if (!empty($cols['reported'])) {
$qb->whereRaw("LOWER(TRIM(COALESCE(reported, ''))) NOT IN ('yes','1','true','y','on')");
}
}
}
+198
View File
@@ -0,0 +1,198 @@
<?php
namespace App\Models;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class AttendanceDay extends Model
{
protected $table = 'attendance_day'; // Because you store created_at/updated_at but CI didn't auto-manage them
public $timestamps = true;
protected $fillable = [
'class_section_id',
'date',
'status',
'submitted_by',
'submitted_at',
'published_by',
'published_at',
'auto_publish_at',
'reopened_by',
'reopened_at',
'reopen_reason',
'semester',
'school_year',
'created_at',
'updated_at',
];
protected $casts = [
'class_section_id' => 'integer',
'date' => 'date:Y-m-d',
'submitted_by' => 'integer',
'published_by' => 'integer',
'reopened_by' => 'integer',
'submitted_at' => 'datetime',
'published_at' => 'datetime',
'auto_publish_at' => 'datetime',
'reopened_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/* ============================================================
* Helpers
* ============================================================
*/
/**
* 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 static function isFinalized(
int $classSectionId,
string $date,
?string $semester = null,
?string $schoolYear = null
): bool {
$q = static::query()
->select('id')
->where('class_section_id', $classSectionId)
->whereDate('date', $date)
->where(function (Builder $w) {
$w->where('status', 'published')
->orWhere('status', 'finalized'); // tolerate legacy data
});
if ($semester !== null) $q->where('semester', $semester);
if ($schoolYear !== null) $q->where('school_year', $schoolYear);
return $q->exists();
}
/**
* Find a day row by (section, date, term). Returns model|null.
*/
public static function findBySectionDate(
int $classSectionId,
string $date,
?string $semester,
?string $schoolYear
): ?self {
return static::query()
->where([
'class_section_id' => $classSectionId,
'date' => $date,
'semester' => $semester,
'school_year' => $schoolYear,
])->first();
}
/**
* 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 static function getOrCreateDraft(
int $classSectionId,
string $date,
?string $semester,
?string $schoolYear,
int $userId = 0
): self {
$now = CarbonImmutable::now('UTC');
// firstOrCreate is already race-safe with a DB unique index:
// if two requests race, one insert wins; the other re-queries.
return static::query()->firstOrCreate(
[
'class_section_id' => $classSectionId,
'date' => $date,
'semester' => $semester,
'school_year' => $schoolYear,
],
[
'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,
'created_at' => $now,
'updated_at' => $now,
]
);
}
/**
* DEPRECATED: Prior code used "finalize" to lock immediately.
* In the new workflow, "finalize" means "teacher submit".
*/
public function finalize(int $userId): bool
{
return $this->submit($userId, null);
}
/**
* Teacher submit: status => submitted (teacher locked, admin editable).
* Optionally set auto_publish_at.
*/
public function submit(int $userId, ?string $autoPublishAt = null): bool
{
$now = CarbonImmutable::now('UTC');
$this->fill([
'status' => 'submitted',
'submitted_by' => $userId,
'submitted_at' => $now,
'auto_publish_at' => $autoPublishAt,
'updated_at' => $now,
]);
return $this->save();
}
/**
* Admin publish (hard lock): status => published.
*/
public function publish(int $userId): bool
{
$now = CarbonImmutable::now('UTC');
$this->fill([
'status' => 'published',
'published_by' => $userId,
'published_at' => $now,
'updated_at' => $now,
]);
return $this->save();
}
/**
* Admin reopen a submitted/published day back to 'draft' (default) or 'submitted'.
*/
public function reopen(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 = CarbonImmutable::now('UTC');
$this->fill([
'status' => $toStatus,
'reopened_by' => $userId,
'reopened_at' => $now,
'reopen_reason' => $reason,
'updated_at' => $now,
]);
return $this->save();
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AttendanceEmailTemplate extends Model
{
protected $table = 'email_templates';
protected $fillable = [
'code',
'variant',
'subject',
'body_html',
'is_active',
'updated_by',
'updated_at',
];
public $timestamps = false;
protected $casts = [
'is_active' => 'boolean',
'updated_by' => 'integer',
'updated_at' => 'datetime',
];
/**
* Fetch an active template by code and variant, falling back to default.
*/
public function getTemplate(string $code, string $variant = 'default'): ?array
{
$row = self::query()
->where('code', $code)
->where('variant', $variant)
->where('is_active', true)
->first();
if ($row) {
return $row->toArray();
}
$row = self::query()
->where('code', $code)
->where('variant', 'default')
->where('is_active', true)
->first();
return $row ? $row->toArray() : null;
}
}
+154
View File
@@ -0,0 +1,154 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AttendanceRecord extends Model
{
protected $table = 'attendance_record';
protected $primaryKey = 'id';
// Enable timestamps using custom CI fields
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
protected $fillable = [
'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 $casts = [
'class_section_id' => 'integer',
'student_id' => 'integer',
'school_id' => 'integer',
'total_absence' => 'integer',
'total_late' => 'integer',
'total_presence' => 'integer',
'total_attendance' => 'integer',
'modified_by' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/*
|--------------------------------------------------------------------------
| Relationships
|--------------------------------------------------------------------------
*/
public function student()
{
return $this->belongsTo(Student::class, 'student_id');
}
public function classSection()
{
return $this->belongsTo(ClassSection::class, 'class_section_id');
}
public function modifier()
{
return $this->belongsTo(User::class, 'modified_by');
}
/*
|--------------------------------------------------------------------------
| Converted CodeIgniter Methods
|--------------------------------------------------------------------------
*/
/**
* Equivalent to CI getAttendanceRecordByClass()
*/
public function getAttendanceRecordByClass($classSectionId, $semester, $schoolYear)
{
return self::where('class_section_id', $classSectionId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->get()
->toArray();
}
/**
* Equivalent to CI getAttendanceRecord()
*/
public function getAttendanceRecord($studentId, $classSectionId, $semester, $schoolYear)
{
return self::where([
'student_id' => $studentId,
'class_section_id' => $classSectionId,
'semester' => $semester,
'school_year' => $schoolYear,
])->first()?->toArray();
}
/**
* Equivalent to CI updateAttendanceRecord()
*/
public function updateAttendanceRecord($attendanceId, $data)
{
return self::where('id', $attendanceId)->update($data) > 0;
}
/**
* Laravel-optimized version of CI incrementAttendanceCounters()
* Uses atomic SQL increments.
*/
public function incrementAttendanceCounters(
int $studentId,
int $classSectionId,
string $semester,
string $schoolYear,
array $increments
): bool {
$record = self::where([
'student_id' => $studentId,
'class_section_id' => $classSectionId,
'semester' => $semester,
'school_year' => $schoolYear,
])->first();
if (!$record) {
return false;
}
foreach ($increments as $field => $value) {
if (in_array($field, [
'total_absence',
'total_late',
'total_presence',
'total_attendance'
])) {
$record->increment($field, $value);
}
}
$record->touch(); // update updated_at
return true;
}
/**
* Equivalent to CI getTotalAbsences()
*/
public function getTotalAbsences($studentId, $semester, $schoolYear): int
{
$record = self::where('student_id', $studentId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->first();
return $record?->total_absence ?? 0;
}
}
+467
View File
@@ -0,0 +1,467 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
use Illuminate\Support\Carbon;
class AttendanceTracking extends BaseModel
{
protected $table = 'attendance_tracking';
protected $primaryKey = 'id';
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
protected $fillable = [
'student_id',
'date',
'is_reported',
'reason',
'note',
'is_notified',
'notif_counter',
'semester',
'school_year',
'created_at',
'updated_at',
];
protected $casts = [
'student_id' => 'integer',
'is_reported' => 'integer',
'is_notified' => 'integer',
'notif_counter' => 'integer',
'date' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/*
|--------------------------------------------------------------------------
| Relationships
|--------------------------------------------------------------------------
*/
public function student()
{
return $this->belongsTo(Student::class, 'student_id');
}
/*
|--------------------------------------------------------------------------
| INTERNAL HELPERS (converted from CI)
|--------------------------------------------------------------------------
*/
private function normalizeDateTime(string $date): string
{
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';
}
return Carbon::parse($date)->toDateTimeString();
}
private function applyTermFilters($query, ?string $semester, ?string $schoolYear)
{
if (!empty($semester)) {
$query->where('semester', $semester);
}
if (!empty($schoolYear)) {
$query->where('school_year', $schoolYear);
}
return $query;
}
private function deriveTermFromDate(string $ymd): array
{
$d = Carbon::parse(substr($ymd, 0, 10));
$month = $d->month;
$year = $d->year;
$semester = ($month >= 8 && $month <= 12) ? 'Fall' : 'Spring';
$startY = ($month >= 8) ? $year : $year - 1;
$endY = $startY + 1;
return [$semester, "{$startY}-{$endY}"];
}
/*
|--------------------------------------------------------------------------
| Main CRUD-ish API (converted from CI)
|--------------------------------------------------------------------------
*/
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 || !$schoolYear) {
[$derivedSemester, $derivedYear] = $this->deriveTermFromDate($dt);
$semester = $semester ?? $derivedSemester;
$schoolYear = $schoolYear ?? $derivedYear;
}
return self::create([
'student_id' => $studentId,
'date' => $dt,
'is_reported' => $isExcused ? 1 : 0,
'reason' => $reason,
'is_notified' => 0,
'notif_counter' => 0,
'semester' => $semester,
'school_year' => $schoolYear,
])->id;
}
public function getStudentAbsences(
int $studentId,
?string $startDate = null,
?string $endDate = null,
?string $semester = null,
?string $schoolYear = null
): array {
$query = self::where('student_id', $studentId);
if ($startDate) {
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
$query->whereDate('date', '>=', $startDate);
} else {
$query->where('date', '>=', $this->normalizeDateTime($startDate));
}
}
if ($endDate) {
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
$query->whereDate('date', '<=', $endDate);
} else {
$query->where('date', '<=', $this->normalizeDateTime($endDate));
}
}
$this->applyTermFilters($query, $semester, $schoolYear);
return $query->orderBy('date', 'DESC')->get()->toArray();
}
public function getUnnotifiedUnexcusedAbsences(
?string $date = null,
?string $semester = null,
?string $schoolYear = null
): array {
$query = self::where('is_reported', 0)->where('is_notified', 0);
if ($date) {
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
$query->whereDate('date', $date);
} else {
$query->where('date', $this->normalizeDateTime($date));
}
}
$this->applyTermFilters($query, $semester, $schoolYear);
return $query->orderBy('date', 'DESC')->get()->toArray();
}
public function hasConsecutiveAbsences(
int $studentId,
int $days,
?string $semester = null,
?string $schoolYear = null
): bool {
$start = Carbon::now()->subDays($days)->toDateString();
$query = self::where('student_id', $studentId)
->whereDate('date', '>=', $start);
$this->applyTermFilters($query, $semester, $schoolYear);
return $query->count() >= $days;
}
public function checkConsecutiveAbsences(
int $studentId,
int $threshold,
?string $semester = null,
?string $schoolYear = null
): bool {
$query = self::where('student_id', $studentId)
->where('is_reported', 0)
->where('is_notified', 0);
$this->applyTermFilters($query, $semester, $schoolYear);
$absences = $query->orderBy('date', 'DESC')
->limit($threshold)
->get()
->toArray();
if (count($absences) < $threshold) {
return false;
}
for ($i = 0; $i < $threshold - 1; $i++) {
$d1 = Carbon::parse($absences[$i]['date']);
$d2 = Carbon::parse($absences[$i + 1]['date']);
if ($d2->diffInDays($d1) !== 7) {
return false;
}
}
return true;
}
public function checkAbsencesInStreak(
int $studentId,
int $absencesThreshold,
int $weeksStreak,
?string $semester = null,
?string $schoolYear = null
): bool {
$query = self::where('student_id', $studentId)
->where('is_reported', 0)
->where('is_notified', 0);
$this->applyTermFilters($query, $semester, $schoolYear);
$records = $query->orderBy('date', 'DESC')
->limit($weeksStreak)
->get()
->toArray();
if (count($records) < $weeksStreak) {
return false;
}
$count = collect($records)->filter(function ($r) {
return $r['is_reported'] == 0 && $r['is_notified'] == 0;
})->count();
return $count >= $absencesThreshold;
}
public function getStudentAttendanceStats(
int $studentId,
?string $semester = null,
?string $schoolYear = null
): array {
$base = self::where('student_id', $studentId);
$this->applyTermFilters($base, $semester, $schoolYear);
$totalAbs = (clone $base)->count();
$unexcusedAbs = (clone $base)->where('is_reported', 0)->count();
$last = (clone $base)->orderBy('date', 'DESC')->first();
return [
'total_absences' => $totalAbs,
'unexcused_absences' => $unexcusedAbs,
'last_absence' => $last ? $last->toArray() : null,
];
}
public function getAttendanceBySemester(
?string $semester = null,
?string $schoolYear = null,
int $limit = 0
): array {
$query = self::query();
$this->applyTermFilters($query, $semester, $schoolYear);
if ($limit > 0) {
$query->limit($limit);
}
return $query->orderBy('date', 'DESC')->get()->toArray();
}
public function getSemesterSummary(?string $semester = null, ?string $schoolYear = null): array
{
$query = self::query();
$this->applyTermFilters($query, $semester, $schoolYear);
$total = (clone $query)->count();
$unexcused = (clone $query)->where('is_reported', 0)->count();
$students = (clone $query)->distinct()->count('student_id');
return [
'total_absences' => $total,
'unexcused_absences' => $unexcused,
'students_with_absences' => $students,
];
}
/*
|--------------------------------------------------------------------------
| Violation Update System
|--------------------------------------------------------------------------
*/
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($absence['date'] ?? now()->format('Y-m-d'), 0, 10);
$dt = $this->normalizeDateTime($ymd);
$data = [
'student_id' => (int)$student['id'],
'date' => $dt,
'is_reported' => 0,
'reason' => $student['violation'] ?? '',
'is_notified' => 0,
'notif_counter' => 0,
'semester' => $semester,
'school_year' => $schoolYear,
];
$existing = self::where('student_id', $data['student_id'])
->whereDate('date', $ymd)
->where('semester', $semester)
->where('school_year', $schoolYear)
->first();
if ($existing) {
if ($existing->reason !== $data['reason']) {
$existing->update($data);
$results['updated']++;
if ($existing->is_notified) {
$existing->update(['is_notified' => 0, 'notif_counter' => 0]);
$results['notified']++;
}
} else {
$results['skipped']++;
}
} else {
$new = self::create($data);
$results['added']++;
if (preg_match('/\b4\b/', $data['reason'])) {
$new->update(['is_notified' => 1, 'notif_counter' => 1]);
$results['notified']++;
}
}
}
}
return $results;
}
/*
|--------------------------------------------------------------------------
| Notification operations
|--------------------------------------------------------------------------
*/
public function markAsNotified(int $id): bool
{
self::where('id', $id)->increment('notif_counter');
return self::where('id', $id)->update(['is_notified' => 1]) > 0;
}
public function resetNotification(int $id): bool
{
return self::where('id', $id)->update([
'is_notified' => 0,
'notif_counter' => 0
]) > 0;
}
public function markRuleAsNotified(
int $studentId,
string $code,
string $ymd,
?string $semester = null,
?string $schoolYear = null
): bool {
$semester = $semester ?? config('school.semester');
$schoolYear = $schoolYear ?? config('school.year');
$start = Carbon::parse($ymd)->startOfDay();
$end = Carbon::parse($ymd)->addDay()->startOfDay();
$row = self::where('student_id', $studentId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->whereBetween('date', [$start, $end])
->where('reason', 'like', "%{$code}%")
->orderBy('date', 'DESC')
->first();
if (!$row) {
$row = self::create([
'student_id' => $studentId,
'date' => $start,
'is_reported' => 0,
'reason' => $code,
'is_notified' => 1,
'notif_counter' => 1,
'semester' => $semester,
'school_year' => $schoolYear,
]);
return true;
}
$count = ($row->notif_counter ?? 0) + 1;
return $row->update([
'is_notified' => 1,
'notif_counter' => $count,
'is_reported' => 0,
'updated_at' => now(),
]);
}
public function markNotified(
int $studentId,
string $schoolYear,
?string $semester,
string $code,
?string $lastDate = null
): int {
$query = self::where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('reason', 'like', "%{$code}%");
if ($semester) {
$query->where('semester', $semester);
}
if ($lastDate) {
$query->where('date', '<=', $lastDate);
}
$row = $query->orderBy('date', 'DESC')->first();
if (!$row) {
return 0;
}
$counter = ($row->notif_counter ?? 0) + 1;
return $row->update([
'is_notified' => 1,
'notif_counter' => $counter,
'updated_at' => now(),
]) ? 1 : 0;
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AuthorizedUser extends Model
{
use HasFactory;
protected $table = 'authorized_users'; // Table name
protected $primaryKey = 'id';
public $incrementing = true;
protected $fillable = [
'user_id',
'authorized_user_id',
'firstname',
'lastname',
'phone_number',
'gender',
'email',
'relation_to_user',
'token',
'status',
'created_at',
'updated_at',
];
// Laravel manages created_at + updated_at automatically
public $timestamps = true;
protected $casts = [
'user_id' => 'integer',
'authorized_user_id' => 'integer',
'email' => 'string',
'firstname' => 'string',
'lastname' => 'string',
'phone_number' => 'string',
'relation_to_user' => 'string',
'token' => 'string',
'status' => 'string',
];
// Relationship: Each authorized user belongs to a user
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Models;
class AuthorizedUsers extends BaseModel
{
protected $table = 'authorized_users';
protected $primaryKey = 'id';
public $timestamps = true;
protected $fillable = [
'user_id',
'authorized_user_id',
'firstname',
'lastname',
'phone_number',
'gender',
'email',
'relation_to_user',
'token',
'status',
'created_at',
'updated_at',
];
}
+118
View File
@@ -0,0 +1,118 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Carbon;
class BadgePrintLog extends Model
{
use HasFactory;
protected $table = 'badge_print_logs';
protected $primaryKey = 'id';
public $timestamps = false; // matches CI model
protected $fillable = [
'user_id',
'printed_by',
'school_year',
'printed_at',
'role',
'class_section_name',
'copies',
];
/**
* Laravel version of CI4 logPrints()
*
* @param array $userIds
* @param int|null $printedBy
* @param string|null $schoolYear
* @param array $roleMap userId => role label
* @param array $classMap userId => class name
* @param int $copies
* @return int rows inserted
*/
public function logPrints(array $userIds, ?int $printedBy, ?string $schoolYear, array $roleMap = [], array $classMap = [], int $copies = 1): int
{
$now = Carbon::now('UTC')->toDateTimeString();
$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 {
DB::table($this->table)->insert($rows);
return count($rows);
} catch (\Throwable $e) {
\Log::error("BadgePrintLog::logPrints failed: " . $e->getMessage());
return 0;
}
}
/**
* Laravel version of CI4 getStatus()
*
* @param array $userIds
* @param string|null $schoolYear
* @return array
*/
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 {
$query = DB::table($this->table . ' as l')
->selectRaw('
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)) {
$query->where('l.school_year', $schoolYear);
}
$rows = $query->get();
} catch (\Throwable $e) {
\Log::error("BadgePrintLog::getStatus failed: " . $e->getMessage());
return [];
}
$result = [];
foreach ($rows as $row) {
$result[(int)$row->user_id] = [
'count' => (int)$row->prints,
'last_printed_at' => (string)$row->last_printed_at,
'last_printed_by' => $row->last_printed_by !== null ? (int)$row->last_printed_by : null,
];
}
return $result;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* BaseModel replaces the CodeIgniter Model and wires up the legacy builder
* and database utility that the existing models expect.
*/
class BaseModel extends Model
{
use HasFactory;
public CiDatabase $db;
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->db = CiDatabase::instance();
}
/**
* Ensure our legacy builder is used so the old helper methods remain available.
*/
public function newEloquentBuilder($query)
{
return new CiModelBuilder($query);
}
/**
* Keep the familiar CodeIgniter helper available on the model itself.
*/
public function findAll($limit = 0, $offset = 0)
{
return $this->newModelQuery()->findAll($limit, $offset);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Models;
class BroadcastEmail extends BaseModel
{
protected $table = 'broadcast_emails';
protected $primaryKey = 'id';
public $timestamps = true;
protected $fillable = [
'subject',
'body',
'total_sent',
'created_at',
];
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Calendar extends Model
{
protected $table = 'calendar_events';
protected $fillable = [
'title',
'date',
'description',
'notify_parent',
'notify_admin',
'notify_teacher',
'no_school',
'semester',
'school_year',
'notification_sent',
'created_at',
'updated_at',
];
public $timestamps = true;
/**
* TODO: Manual port review required for custom CI query methods from Calendar.
* - getEvents()
* - getEventByDate()
* - addEvent()
* - getEventsBySchoolYear()
* - getEventsBySchoolYearAndSemester()
* - supportsEventType()
*
* This automated conversion preserves schema metadata only.
*/
}
+127
View File
@@ -0,0 +1,127 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CalendarEvent extends Model
{
use HasFactory;
protected $table = 'calendar_events';
protected $primaryKey = 'id';
protected $fillable = [
'title',
'date',
'description',
'notify_parent',
'notify_admin',
'notify_teacher',
'no_school',
'semester',
'school_year',
'notification_sent',
'created_at',
'updated_at',
];
// Equivalent to CI4 timestamps
public $timestamps = true;
protected $casts = [
'date' => 'date',
'notify_parent' => 'boolean',
'notify_admin' => 'boolean',
'notify_teacher'=> 'boolean',
'no_school' => 'boolean',
];
/* ----------------------------------------------------------------------
* GETTERS Same behavior as CodeIgniter
* ---------------------------------------------------------------------- */
public function getEvents(): array
{
$events = $this->orderBy('date', 'ASC')->get()->toArray();
return $this->withNoSchoolFlag($events);
}
public function getEventByDate($date): array
{
$events = $this->whereDate('date', $date)->get()->toArray();
return $this->withNoSchoolFlag($events);
}
public function getEventsBySchoolYear(string $schoolYear): array
{
$events = $this->where('school_year', $schoolYear)
->orderBy('date', 'ASC')
->get()
->toArray();
return $this->withNoSchoolFlag($events);
}
public function getEventsBySchoolYearAndSemester(string $schoolYear, ?string $semester = null): array
{
$query = $this->where('school_year', $schoolYear);
if (!empty($semester)) {
$query->where('semester', $semester);
}
$events = $query->orderBy('date', 'ASC')->get()->toArray();
return $this->withNoSchoolFlag($events);
}
/* ----------------------------------------------------------------------
* CREATE EVENT — Eloquent version of addEvent()
* ---------------------------------------------------------------------- */
public function addEvent(array $data)
{
return $this->create($data);
}
/* ----------------------------------------------------------------------
* UTILITIES — SAME LOGIC YOU HAD IN CI4
* ---------------------------------------------------------------------- */
/**
* Adds computed no_school flag if missing.
*/
protected function withNoSchoolFlag(array $events): array
{
return array_map(function ($event) {
if (array_key_exists('no_school', $event)) {
$event['no_school'] = (int)$event['no_school'];
} else {
$event['no_school'] = $this->isNoSchoolEvent($event) ? 1 : 0;
}
return $event;
}, $events);
}
/**
* Heuristic detection of no-school days from text.
*/
protected function isNoSchoolEvent(array $event): bool
{
$title = strtolower($event['title'] ?? '');
$desc = strtolower($event['description'] ?? '');
$text = $title . ' ' . $desc;
$keywords = ['no school', 'no-school', 'cancel', 'vacation', 'holiday', 'break'];
foreach ($keywords as $kw) {
if ($kw !== '' && str_contains($text, $kw)) {
return true;
}
}
return false;
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
namespace App\Models;
use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB;
class CiDatabase
{
protected static ?self $instance = null;
protected Connection $connection;
protected int $lastAffectedRows = 0;
public static function instance(): self
{
return static::$instance ??= new static();
}
protected function __construct()
{
$this->connection = DB::connection();
}
public function table(string $table): CiQueryBuilder
{
$normalized = $this->normalizeTableName($table);
return new CiQueryBuilder($this->connection->table($normalized));
}
public function query(string $sql, array $params = []): CiQueryResult
{
$trimmed = ltrim($sql);
if ($trimmed === '') {
return new CiQueryResult([]);
}
$first = strtolower(strtok($trimmed, " \t\n\r\0\x0B") ?: '');
if ($first === 'select') {
$rows = $this->connection->select($sql, $params);
$this->lastAffectedRows = 0;
return new CiQueryResult($rows);
}
$affected = $this->connection->affectingStatement($sql, $params);
$this->lastAffectedRows = $affected;
return new CiQueryResult([]);
}
public function escape($value): string
{
return $this->connection->getPdo()->quote($value);
}
public function affectedRows(): int
{
return $this->lastAffectedRows;
}
public function getFieldNames(string $table): array
{
return $this->connection->getSchemaBuilder()->getColumnListing($table);
}
protected function normalizeTableName(string $table): string
{
$normalized = trim($table);
if (stripos($normalized, ' as ') !== false) {
return $normalized;
}
if (strpos($normalized, ' ') !== false) {
[$name, $alias] = preg_split('/\s+/', $normalized, 2);
return "{$name} as {$alias}";
}
return $normalized;
}
}
+105
View File
@@ -0,0 +1,105 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
class CiModelBuilder extends Builder
{
protected array $setData = [];
public function get($columns = ['*'])
{
return new CiQueryResult(parent::get($columns));
}
public function first($columns = ['*'])
{
$model = parent::first($columns);
return $model;
}
public function findAll(int $limit = 0, int $offset = 0)
{
if ($limit > 0) {
$this->limit($limit);
}
if ($offset > 0) {
$this->offset($offset);
}
return $this->getResultArray();
}
public function getResultArray(): array
{
return $this->get()->getResultArray();
}
public function getRowArray(): ?array
{
return $this->get()->getRowArray();
}
public function getRow(?string $column = null)
{
return $this->get()->getRow($column);
}
public function countAllResults(): int
{
return $this->count();
}
public function like(string $column, $match, string $side = 'both')
{
$pattern = $this->wrapLike($match, $side);
$this->query->where($column, 'like', $pattern);
return $this;
}
public function set($key, $value = null, bool $escape = true)
{
if (is_array($key)) {
$this->setData = array_merge($this->setData, $key);
} else {
$this->setData[$key] = $value;
}
return $this;
}
public function update(array $values = [])
{
$data = $values ?: $this->setData;
$this->setData = [];
return parent::update($data);
}
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if ($operator !== null && $value === null && preg_match('/^(.*)\\s(>=|<=|<>|!=|>|<)$/', $column, $matches)) {
return parent::where(trim($matches[1]), $matches[2], $operator, $boolean);
}
return parent::where($column, $operator, $value, $boolean);
}
protected function wrapLike($match, string $side): string
{
$pattern = "%{$match}%";
switch (strtolower($side)) {
case 'before':
$pattern = "%{$match}";
break;
case 'after':
$pattern = "{$match}%";
break;
}
return $pattern;
}
}
+153
View File
@@ -0,0 +1,153 @@
<?php
namespace App\Models;
use Illuminate\Database\Query\Builder as QueryBuilder;
class CiQueryBuilder
{
protected QueryBuilder $builder;
protected array $setData = [];
protected array $tableMethods = [
'join',
'leftJoin',
'rightJoin',
'crossJoin',
'from',
'table',
'joinWhere',
'leftJoinWhere',
];
public function __construct(QueryBuilder $builder)
{
$this->builder = $builder;
}
public function __call($method, $arguments)
{
$arguments = $this->normalizeTableArgument($method, $arguments);
$result = $this->builder->$method(...$arguments);
if ($result instanceof QueryBuilder) {
$this->builder = $result;
return $this;
}
return $result;
}
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if ($operator !== null && $value === null && preg_match('/^(.*)\s(>=|<=|<>|!=|>|<)$/', $column, $matches)) {
return $this->where(trim($matches[1]), $matches[2], $operator, $boolean);
}
$this->builder = $this->builder->where($column, $operator, $value, $boolean);
return $this;
}
public function like($column, $match, string $side = 'both')
{
$pattern = $this->wrapLike($match, $side);
$this->builder = $this->builder->where($column, 'like', $pattern);
return $this;
}
public function set($key, $value = null, bool $escape = true)
{
if (is_array($key)) {
$this->setData = array_merge($this->setData, $key);
} else {
$this->setData[$key] = $value;
}
return $this;
}
public function update($values = [])
{
$data = $values ?: $this->setData;
$this->setData = [];
return $this->builder->update($data);
}
public function findAll(int $limit = 0, int $offset = 0)
{
if ($limit > 0) {
$this->builder = $this->builder->limit($limit);
}
if ($offset > 0) {
$this->builder = $this->builder->offset($offset);
}
return $this->getResultArray();
}
public function get($columns = ['*'])
{
$collection = $this->builder->get($columns);
return new CiQueryResult($collection);
}
public function getResultArray(): array
{
return $this->get()->getResultArray();
}
public function getRowArray(): ?array
{
return $this->get()->getRowArray();
}
public function getRow(?string $column = null)
{
return $this->get()->getRow($column);
}
public function countAllResults(): int
{
return $this->builder->count();
}
protected function wrapLike($match, string $side): string
{
$pattern = "%{$match}%";
switch (strtolower($side)) {
case 'before':
return "%{$match}";
case 'after':
return "{$match}%";
default:
return $pattern;
}
}
protected function normalizeTableArgument(string $method, array $arguments): array
{
if (in_array($method, $this->tableMethods, true) && isset($arguments[0]) && is_string($arguments[0])) {
$arguments[0] = $this->normalizeTableName($arguments[0]);
}
return $arguments;
}
protected function normalizeTableName(string $table): string
{
$normalized = trim($table);
if (stripos($normalized, ' as ') !== false) {
return $normalized;
}
if (strpos($normalized, ' ') !== false) {
[$name, $alias] = preg_split('/\s+/', $normalized, 2);
return "{$name} as {$alias}";
}
return $normalized;
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class CiQueryResult extends Collection
{
public function __construct($items = [])
{
if ($items instanceof Collection) {
$items = $items->all();
}
parent::__construct(array_map([$this, 'castRow'], (array) $items));
}
protected function castRow($row): array
{
if ($row instanceof Model) {
return $row->toArray();
}
if (is_object($row)) {
return (array) $row;
}
return (array) $row;
}
public function getResultArray(): array
{
return $this->toArray();
}
public function getRowArray(): ?array
{
$first = $this->first();
return $first ? (array) $first : null;
}
public function getRow(?string $column = null)
{
$row = $this->getRowArray();
if ($row === null) {
return null;
}
if ($column === null) {
return $row;
}
return $row[$column] ?? null;
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
class ClassModel extends BaseModel
{
protected $table = 'classes';
protected $primaryKey = 'id';
public $timestamps = false;
protected $fillable = [
'class_name',
'schedule',
'capacity',
'semester',
'school_year',
];
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ClassPrepAdjustment extends Model
{
protected $table = 'class_prep_adjustments';
protected $fillable = [
'class_section_id',
'item_name',
'adjustment',
'adjustable',
'school_year',
'created_at',
];
public $timestamps = false;
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
class ClassPreparation extends BaseModel
{
protected $table = 'class_preparation';
protected $primaryKey = 'id';
public $timestamps = true;
protected $fillable = [
'class_id',
'school_year',
'requirements',
'created_at',
'updated_at',
];
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ClassPreparationLog extends Model
{
protected $table = 'class_preparation_log';
protected $fillable = [
'class_section_id',
'class_section',
'school_year',
'prep_data',
'created_at',
];
public $timestamps = false;
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ClassProgressAttachment extends Model
{
protected $table = 'class_progress_attachments';
protected $fillable = [
'report_id',
'file_path',
'original_name',
'mime_type',
'file_size',
'created_at',
];
public $timestamps = false;
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ClassProgressReport extends Model
{
protected $table = 'class_progress_reports';
protected $fillable = [
'class_section_id',
'teacher_id',
'week_start',
'week_end',
'subject',
'unit_title',
'materials',
'covered',
'homework',
'assessment',
'status',
'status_notes',
'class_notes',
'next_week_plan',
'support_needed',
'flags_json',
'attachment_path',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class ClassSection extends BaseModel
{
protected $table = 'classSection'; // Correct table name
protected $primaryKey = 'id'; // Specify the primary key field
protected $fillable = [
'class_id',
'class_section_id',
'class_section_name',
'created_at',
'updated_at',
'semester',
'school_year',
];
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
// Set default return type
// Method to get class sections with class and section names
public function getClassSections(?string $schoolYear = null, ?string $semester = null)
{
$builder = $this->select('ClassSection.*', 'classes.class_name', 'sections.section_name')
->join('classes', 'ClassSection.class_id', '=', 'classes.id')
->leftJoin('sections', 'ClassSection.class_section_id', '=', 'sections.id');
if ($schoolYear !== null) {
$builder->where('ClassSection.school_year', $schoolYear);
}
if ($semester !== null) {
$builder->where('ClassSection.semester', $semester);
}
return $builder->findAll();
}
public function getCurrentAcademicSection(int $classId, string $schoolYear, string $semester): ?array
{
return $this->newQuery()
->where('class_id', $classId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->orderBy('class_section_name', 'ASC')
->first();
}
/**
* 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();
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Classroom extends Model
{
use HasFactory;
protected $table = 'classes';
protected $primaryKey = 'id';
protected $fillable = [
'class_name',
'schedule',
'capacity',
'semester',
'school_year',
];
public $timestamps = false;
/* -----------------------------------------------
* Equivalent CI4 functions rewritten for Laravel
* ----------------------------------------------- */
public function getAllClasses()
{
return self::all();
}
public function getClassById($id)
{
return self::find($id);
}
public function createClass(array $data)
{
return self::create($data); // returns the model instance
}
public function updateClass($id, array $data)
{
$class = self::find($id);
if (!$class) {
return false;
}
return $class->update($data);
}
public function deleteClass($id)
{
return self::destroy($id);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CommunicationLog extends Model
{
protected $table = 'communication_logs';
protected $fillable = [
'student_id',
'family_id',
'student_name',
'template_key',
'subject',
'body',
'recipients',
'cc',
'bcc',
'attachments',
'status',
'error_message',
'sent_by',
'metadata',
];
public $timestamps = false;
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Competition extends Model
{
use SoftDeletes;
protected $table = 'competitions';
protected $fillable = [
'title',
'semester',
'school_year',
'class_section_id',
'start_date',
'end_date',
'winners_count',
'prize_per_point',
'is_published',
'published_at',
'created_by',
'created_at',
'updated_at',
'deleted_at',
'is_locked',
'locked_at',
'locked_by',
];
public $timestamps = true;
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CompetitionClassWinner extends Model
{
protected $table = 'competition_class_winners';
protected $fillable = [
'competition_id',
'class_section_id',
'winners_override',
'created_at',
'updated_at',
'question_count',
'prize_1',
'prize_2',
'prize_3',
'prize_4',
'prize_5',
'prize_6',
];
public $timestamps = true;
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CompetitionScore extends Model
{
protected $table = 'competition_scores';
protected $fillable = [
'competition_id',
'student_id',
'class_section_id',
'score',
'notes',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CompetitionWinner extends Model
{
protected $table = 'competition_winners';
protected $fillable = [
'competition_id',
'student_id',
'class_section_id',
'rank',
'score',
'prize_amount',
'created_at',
];
public $timestamps = false;
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Configuration extends BaseModel
{
protected $table = 'configuration'; // The name of the table
protected $primaryKey = 'id'; // The primary key of the table
protected $fillable = [
'config_key',
'config_value',
];
public $timestamps = false;
/**
* 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)
{
return $this->getConfigValueByKey((string) $key);
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class ContactUs extends BaseModel
{
protected $table = 'contactus';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $useSoftDeletes = false;
protected $fillable = [
'sender_id',
'reciever_id',
'subject',
'message',
'created_at',
'updated_at',
'semester',
'school_year',
];
// Timestamps (set manually or use auto timestamps below)
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CurrentFlag extends Model
{
protected $table = 'current_flag';
protected $fillable = [
'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',
];
public $timestamps = true;
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class DiscountUsage extends BaseModel
{
protected $table = 'discount_usages';
protected $primaryKey = 'id';
protected $fillable = [
'voucher_id',
'invoice_id',
'parent_id',
'discount_amount',
'description',
'school_year',
'updated_by',
'used_at',
'created_at',
'updated_at',
'semester',
];
// Enable automatic timestamps if desired (optional: only if you add created_at/updated_at columns)
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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;
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class DiscountVoucher extends BaseModel
{
protected $table = 'discount_vouchers';
protected $primaryKey = 'id';
protected $fillable = [
'code',
'discount_type',
'description',
'discount_value',
'max_uses',
'times_used',
'valid_from',
'valid_until',
'is_active',
'created_at',
'updated_at',
'school_year',
'semester',
];
// Timestamps managed by CI
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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();
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class EarlyDismissalSignature extends Model
{
protected $table = 'early_dismissal_signatures';
protected $fillable = [
'report_date',
'school_year',
'semester',
'filename',
'original_name',
'mime_type',
'file_size',
'uploaded_by',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class EmailTemplate extends Model
{
protected $table = 'email_templates'; // Your CI model only had updated_at (no created_at)
public $timestamps = false;
protected $fillable = [
'code',
'variant',
'subject',
'body_html',
'is_active',
'updated_by',
'updated_at',
];
protected $casts = [
'is_active' => 'boolean',
'updated_by' => 'integer',
'updated_at' => 'datetime',
];
/**
* Fetch a template by code and variant, falling back to 'default' if needed.
* Active only.
*/
public static function getTemplate(string $code, string $variant = 'default'): ?self
{
// Try exact active
$row = static::query()
->where('code', $code)
->where('variant', $variant)
->where('is_active', true)
->first();
if ($row) return $row;
// Fallback to default variant
return static::query()
->where('code', $code)
->where('variant', 'default')
->where('is_active', true)
->first();
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class EmergencyContact extends BaseModel
{
protected $table = 'emergency_contacts';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $fillable = [
'emergency_contact_name',
'parent_id',
'cellphone',
'email',
'relation',
'semester',
'school_year',
'created_at',
'updated_at',
];
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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();
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Enrollment extends BaseModel
{
protected $table = 'enrollments';
protected $primaryKey = 'id';
protected $fillable = [
'student_id',
'class_section_id',
'parent_id',
'enrollment_date',
'enrollment_status',
'withdrawal_date',
'is_withdrawn',
'admission_status',
'semester',
'school_year',
'created_at',
'updated_at',
];
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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;
}
}
+184
View File
@@ -0,0 +1,184 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Event extends BaseModel
{
protected $table = 'events';
protected $primaryKey = 'id';
protected $fillable = [
'event_name',
'event_category',
'description',
'amount',
'flyer',
'expiration_date',
'semester',
'school_year',
'created_by',
'created_at',
'updated_at',
];
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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);
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class EventCharges extends BaseModel
{
protected $table = 'event_charges';
protected $primaryKey = 'id';
protected $fillable = [
'event_id',
'parent_id',
'student_id',
'participation',
'charged',
'semester',
'school_year',
'updated_by',
'created_at',
'updated_at',
];
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
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();
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Exam extends Model
{
protected $table = 'exams';
protected $fillable = [
'student_id',
'student_school_id',
'class_section_id',
'exam_name',
'created_at',
];
public $timestamps = false;
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ExamDraft extends Model
{
protected $table = 'exam_drafts';
protected $fillable = [
'teacher_id',
'class_section_id',
'semester',
'school_year',
'exam_type',
'draft_title',
'description',
'teacher_file',
'teacher_filename',
'status',
'admin_id',
'admin_comments',
'reviewed_at',
'final_file',
'final_filename',
'version',
'previous_draft_id',
'is_legacy',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Expense extends BaseModel
{
protected $table = 'expenses';
protected $primaryKey = 'id';
protected $fillable = [
'category',
'amount',
'receipt_path',
'description',
'retailor',
'purchased_by',
'date_of_purchase',
'added_by',
'created_at',
'updated_at',
'school_year',
'semester',
'status',
'status_reason',
'updated_by',
'approved_by',
'reimbursement_id',
];
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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,
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;
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Family extends Model
{
protected $table = 'families';
protected $fillable = [
'family_code',
'household_name',
'address_line1',
'address_line2',
'city',
'state',
'postal_code',
'country',
'primary_phone',
'preferred_lang',
'preferred_contact_method',
'is_active',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class FamilyCommPref extends Model
{
protected $table = 'family_comm_prefs';
protected $fillable = [
'family_id',
'category',
'via_email',
'via_sms',
'cc_all_guardians',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class FamilyGuardian extends Model
{
protected $table = 'family_guardians';
protected $fillable = [
'family_id',
'user_id',
'relation',
'is_primary',
'receive_emails',
'receive_sms',
'custody_notes',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class FamilyStudent extends BaseModel {
protected $table = 'family_students';
protected $primaryKey = 'id';
protected $fillable = [
'family_id',
'student_id',
'is_primary_home',
'notes',
'created_at',
'updated_at',
];
public $timestamps = true;
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();
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class FinalExam extends BaseModel
{
protected $table = 'final_exam';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $fillable = [
'student_id',
'school_id',
'class_section_id',
'updated_by',
'score',
'semester',
'school_year',
'created_at',
'updated_at',
];
protected bool $allowEmptyInserts = false;
protected bool $updateOnlyChanged = true;
protected $casts = [];
protected $castHandlers = [];
// Dates
public $timestamps = true;
protected $dateFormat = 'datetime';
const CREATED_AT = 'created_at';
const UPDATED_AT = '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)
{
// 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;
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class FinalScore extends BaseModel
{
protected $table = 'final_score';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $fillable = [
'student_id',
'school_id',
'class_section_id',
'updated_by',
'score',
'score_letter',
'comment',
'school_year',
'created_at',
'updated_at',
'semester',
];
public $timestamps = true;
// 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;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flag extends Model
{
protected $table = 'flag';
protected $fillable = [
'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',
];
public $timestamps = true;
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class GradingLock extends Model
{
protected $table = 'grading_locks';
protected $fillable = [
'class_section_id',
'semester',
'school_year',
'is_locked',
'locked_by',
'locked_at',
'created_at',
'updated_at',
];
public $timestamps = true;
/**
* TODO: Manual port review required for custom CI query methods from GradingLock.
* - getLock()
* - isLocked()
*
* This automated conversion preserves schema metadata only.
*/
}
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Homework extends BaseModel
{
protected $table = 'homework';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $fillable = [
'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 $casts = [];
protected $castHandlers = [];
// Dates
public $timestamps = true;
protected $dateFormat = 'datetime';
const CREATED_AT = 'created_at';
const UPDATED_AT = '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')
* @return float The average score (0.0 if no homework found)
*/
public function getAverageHomeworkScore(int $studentId, string $semester, string $schoolYear): float
{
// Build the query using Query Builder
$builder = $this->db->table('homework');
$builder->selectAvg('score', 'average_score')
->where('student_id', $studentId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->where('score IS NOT NULL'); // Only include records with actual scores
// Execute the query
$result = $builder->get()->getRow();
// Return the average or 0.0 if no results
return $result ? (float)$result->average_score : 0.0;
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class InventoryCategory extends Model
{
protected $table = 'inventory_categories';
protected $fillable = [
'type',
'name',
'description',
'grade_min',
'grade_max',
'created_at',
'updated_at',
];
public $timestamps = true;
/**
* TODO: Manual port review required for custom CI query methods from InventoryCategory.
* - optionsForType()
*
* This automated conversion preserves schema metadata only.
*/
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class InventoryItem extends BaseModel
{
protected $table = 'inventory_items';
protected $primaryKey = 'id';
protected $useSoftDeletes = false;
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
// IMPORTANT: include everything you ever update/insert
protected $fillable = [
'type',
'category_id',
'name',
'description',
'quantity',
'good_qty',
'needs_repair_qty',
'need_replace_qty',
'cannot_find_qty',
'unit',
'condition',
'isbn',
'edition',
'sku',
'notes',
'semester',
'school_year',
'updated_by',
'created_at',
'updated_at',
];
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
];
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class InventoryMovement extends Model
{
protected $table = 'inventory_movements';
protected $fillable = [
'item_id',
'qty_change',
'movement_type',
'reason',
'note',
'semester',
'school_year',
'performed_by',
'teacher_id',
'student_id',
'class_section_id',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+298
View File
@@ -0,0 +1,298 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Invoice extends BaseModel
{
protected $table = 'invoices';
protected $primaryKey = 'id';
protected $fillable = [
'parent_id',
'invoice_number',
'total_amount',
'balance',
'paid_amount',
'has_discount',
'issue_date',
'due_date',
'status',
'description',
'school_year',
'created_at',
'updated_at',
'updated_by',
'semester',
];
// Your table handles timestamps (defaults/triggers), keep CI auto off
public $timestamps = true;
// --- 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->where('parent_id', $userId);
if ($schoolYear !== null) {
$builder->where('school_year', $schoolYear);
}
return $builder->findAll();
}
public function updateInvoiceStatus($invoiceId, $status)
{
return $this->update($invoiceId, ['status' => $status]);
}
public function getUnpaidInvoices()
{
return $this->where('status', 'unpaid')
->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->where('parent_id', $parentId);
if (!empty($schoolYear)) {
$builder->where('school_year', $schoolYear);
}
return $builder->orderBy('created_at', 'DESC')->findAll();
}
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 [];
return $this->select('id,parent_id,invoice_number,status,balance,total_amount,issue_date,due_date')
->whereIn('parent_id', array_map('intval', $parentIds))
->where('school_year', $schoolYear)
->orderBy('issue_date', 'DESC')
->orderBy('id', 'DESC')
->findAll();
}
/* =======================
* 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);
return $this->select('id, parent_id, invoice_number, status, balance, issue_date')
->whereIn('parent_id', $parentIds)
->where('school_year', $schoolYear)
->orderBy('issue_date', 'DESC')
->orderBy('id', 'DESC')
->findAll();
}
/**
* 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();
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class InvoiceEvent extends BaseModel
{
protected $table = 'invoice_event';
protected $primaryKey = 'id';
protected $fillable = [
'invoice_id',
'event_name',
'description',
'amount',
'semester',
'school_year',
'updated_by',
'created_at',
'updated_at',
];
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class InvoiceStudentList extends BaseModel
{
// 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 $fillable = [
'invoice_id',
'student_id',
'student_firstname',
'student_lastname',
'school_id',
'enrolled',
'school_year',
'created_at',
'updated_at',
'semester',
];
// Enable auto-incrementing primary key
protected $useAutoIncrement = true;
// Specify return type as an array
// Automatically set `created_at` and `updated_at` fields
public $timestamps = true;
// Specify the names of the timestamp fields
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class IpAttempt extends BaseModel
{
protected $table = 'ip_attempts';
protected $primaryKey = 'id';
protected $fillable = [
'ip_address',
'attempts',
'last_attempt_at',
'blocked_until',
'created_at',
'updated_at',
'school_year',
'semester',
];
// Automatically handle timestamps
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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);
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class LateSlipLog extends BaseModel
{
protected $table = 'late_slip_logs';
protected $primaryKey = 'id';
public $timestamps = false; // we control printed_at explicitly
protected $fillable = [
'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', 'LateSlipLog::logSlip failed: ' . $e->getMessage());
return false;
}
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class LoginActivity extends BaseModel
{
protected $table = 'login_activity';
protected $primaryKey = 'id';
protected $fillable = [
'user_id',
'email',
'login_time',
'logout_time',
'ip_address',
'user_agent',
'created_at',
'updated_at',
'semester',
'school_year',
];
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
public function getLastActivities($limit = 4)
{
return $this->orderBy('login_time', 'DESC')->findAll($limit);
}
}
?>
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class ManualPayment extends BaseModel
{
protected $table = 'manual_payments';
protected $fillable = [
'invoice_number',
'amount',
'payment_method',
'reference',
'discount_number',
'proof_path',
'created_at',
'school_year',
'semester',
];
public $timestamps = false;
}
+157
View File
@@ -0,0 +1,157 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Message extends BaseModel
{
protected $table = 'messages';
protected $primaryKey = 'id';
protected $fillable = [
'sender_id',
'recipient_id',
'subject',
'message',
'sent_datetime',
'read_status',
'read_datetime',
'message_number',
'priority',
'attachment',
'status',
'semester',
'school_year',
];
public $timestamps = false; // Since you're manually handling date fields
/**
* 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();
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class MidtermExam extends BaseModel
{
protected $table = 'midterm_exam';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $fillable = [
'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 $casts = [];
protected $castHandlers = [];
// Dates
public $timestamps = true;
protected $dateFormat = 'datetime';
const CREATED_AT = 'created_at';
const UPDATED_AT = '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 getMidtermExamScore($studentId, $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('school_year', $schoolYear)
->first(); // Get the first matching result
// Return the score, or null if no result is found
return $result ? $result['score'] : null;
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class MissingScoreOverride extends Model
{
protected $table = 'missing_score_overrides';
protected $fillable = [
'student_id',
'class_section_id',
'semester',
'school_year',
'item_type',
'item_index',
'is_allowed',
'updated_by',
'created_at',
'updated_at',
];
public $timestamps = true;
/**
* TODO: Manual port review required for custom CI query methods from MissingScoreOverride.
* - getOverridesMap()
* - replaceOverrides()
*
* This automated conversion preserves schema metadata only.
*/
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class NavItem extends BaseModel
{
protected $table = 'nav_items';
protected $primaryKey = 'id';
public $timestamps = true;
protected $useSoftDeletes = true;
protected $fillable = [
'menu_parent_id',
'label',
'url',
'icon_class',
'target',
'sort_order',
'is_enabled',
'created_at',
'updated_at',
'deleted_at',
];
public function getChildrenOf(int $parentId): array
{
return $this->where('parent_id', $parentId)
->where('is_enabled', 1)
->orderBy('sort_order', 'ASC')
->findAll();
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Notification extends BaseModel
{
protected $table = 'notifications';
protected $primaryKey = 'id';
protected $fillable = [
'title',
'message',
'target_group',
'delivery_channels',
'priority',
'status',
'action_url',
'attachment_path',
'scheduled_at',
'sent_at',
'expires_at',
'created_at',
'updated_at',
'deleted_at',
'school_year',
'semester',
];
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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();
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class ParentAttendanceReport extends BaseModel
{
protected $table = 'parent_attendance_reports';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
protected $fillable = [
'parent_id',
'student_id',
'class_section_id',
'report_date',
'type',
'arrival_time',
'dismiss_time',
'reason',
'semester',
'school_year',
'status',
'created_at',
'updated_at',
];
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();
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ParentMeetingSchedule extends Model
{
protected $table = 'parent_meeting_schedules';
protected $fillable = [
'student_id',
'parent_user_id',
'parent_name',
'student_name',
'class_section_name',
'date',
'time',
'notes',
'semester',
'school_year',
'status',
'created_by',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class ParentModel extends BaseModel
{
protected $table = 'parents';
protected $primaryKey = 'id';
protected $fillable = [
'secondparent_firstname',
'secondparent_lastname',
'secondparent_gender',
'secondparent_email',
'secondparent_phone',
'firstparent_id',
'secondparent_id',
'semester',
'school_year',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class ParentNotification extends BaseModel
{
protected $table = 'parent_notifications';
protected $primaryKey = 'id';
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
protected $fillable = [
'student_id',
'code',
'incident_date',
'channel',
'to_address',
'subject',
'status',
'response',
'semester',
'school_year',
'created_at',
'updated_at',
];
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';
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Participation extends BaseModel
{
protected $table = 'participation';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $fillable = [
'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 $casts = [];
protected $castHandlers = [];
// Dates
public $timestamps = true;
protected $dateFormat = 'datetime';
const CREATED_AT = 'created_at';
const UPDATED_AT = '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): ?float
{
$result = $this->select('score')
->where('student_id', $studentId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->first();
return $result ? floatval($result['score']) : null;
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
// app/Models/PasswordReset.php
namespace App\Models;
use App\Models\BaseModel;
class PasswordReset extends BaseModel
{
protected $table = 'password_resets';
protected $primaryKey = 'id';
protected $fillable = [
'email',
'token',
'created_at',
'expires_at',
];
public $timestamps = false;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'expires_at';
}
+15
View File
@@ -0,0 +1,15 @@
<?php
// app/Models/PasswordResetRequest.php
namespace App\Models;
use App\Models\BaseModel;
class PasswordResetRequest extends BaseModel
{
protected $table = 'password_reset_requests';
protected $fillable = [
'ip_address',
'requested_at',
];
public $timestamps = false;
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class PayPalPayment extends BaseModel
{
protected $table = 'paypal_payments';
protected $primaryKey = 'id';
protected $fillable = [
'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',
'sync_attempts',
'created_at',
];
// Enable timestamps
public $timestamps = false;
const CREATED_AT = 'created_at';
const UPDATED_AT = ''; // not used; leave empty if not needed
}
+210
View File
@@ -0,0 +1,210 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Payment extends BaseModel
{
protected $table = 'payments';
protected $primaryKey = 'id';
protected $fillable = [
'parent_id',
'invoice_id',
'total_amount',
'paid_amount',
'balance',
'number_of_installments',
'transaction_id',
'check_file',
'check_number',
'payment_method',
'payment_date',
'semester',
'school_year',
'status',
'updated_by',
'created_at',
'updated_at',
];
public $timestamps = true;
// ❗ 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', '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', 'Partially Paid'])
->get()
->getResultArray();
return $rows;
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class PaymentError extends BaseModel
{
protected $table = 'payment_error'; // The DB table
protected $primaryKey = 'id'; // Primary key
protected $fillable = [
'payment_id',
'invoice_id',
'parent_id',
'wrong_paid_amount',
'wrong_payment_method',
'wrong_check_file',
'error_note',
'logged_at',
'logged_by',
'school_year',
'semester',
];
public $timestamps = 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'
];
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class PaymentNotificationLog extends BaseModel
{
protected $table = 'payment_notification_logs';
protected $primaryKey = 'id';
protected $useSoftDeletes = false;
protected $fillable = [
'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',
];
public $timestamps = 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();
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class PaymentTransaction extends BaseModel
{
protected $table = 'payment_transactions'; // Table name
protected $primaryKey = 'id'; // Primary key
// Allowed fields for mass assignment
protected $fillable = [
'transaction_id',
'payment_id',
'transaction_date',
'amount',
'payment_method',
'payment_status',
'transaction_fee',
'school_year',
'semester',
];
// Set automatic timestamps
public $timestamps = false;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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();
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class PaypalTransaction extends BaseModel
{
protected $table = 'paypal_transactions';
protected $primaryKey = 'id';
protected $fillable = [
'transaction_id',
'payment_id',
'firstname',
'lastname',
'event_type',
'payer_email',
'amount',
'currency',
'status',
'payment_method',
'transaction_fee',
'raw_data',
'created_at',
'updated_at',
'school_year',
'semester',
];
public $timestamps = true;
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Permission extends BaseModel
{
protected $table = 'permissions'; // Define the table associated with this model
protected $primaryKey = 'id'; // Define the primary key of the table
protected $fillable = [
'name',
'description',
'created_at',
'updated_at',
]; // Define the fields that can be set using insert/update methods
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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;
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PlacementBatch extends Model
{
protected $table = 'placement_batches';
protected $fillable = [
'placement_test',
'school_year',
'created_by',
'updated_by',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PlacementLevel extends Model
{
protected $table = 'placement_levels';
protected $fillable = [
'student_id',
'level',
'school_year',
'created_by',
'updated_by',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PlacementScore extends Model
{
protected $table = 'placement_scores';
protected $fillable = [
'batch_id',
'student_id',
'score',
'created_by',
'updated_by',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Preferences extends BaseModel
{
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 $fillable = [
'user_id',
'notification_email',
'notification_sms',
'theme',
'language',
'style_color',
'menu_color',
'menu_custom_bg',
'menu_custom_text',
'menu_custom_mode',
'created_at',
'updated_at',
];
// Enable automatic timestamp management if needed
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PrintRequest extends Model
{
protected $table = 'print_requests';
protected $fillable = [
'teacher_id',
'admin_id',
'class_id',
'file_path',
'page_selection',
'num_copies',
'required_by',
'pickup_method',
'status',
'created_at',
'updated_at',
];
public $timestamps = true;
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Project extends BaseModel
{
protected $table = 'project';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $fillable = [
'student_id',
'school_id',
'class_section_id',
'updated_by',
'project_index',
'score',
'comment',
'semester',
'school_year',
'created_at',
'updated_at',
];
public $timestamps = true;
/**
* 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').
* @return float The average project score, or 0 if no scores are found.
*/
public function getAverageProjectScore(int $studentId, string $semester, string $schoolYear): 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);
// Execute the query and get the results
$query = $builder->get();
$results = $query->getResultArray();
// If there are no scores, return 0
if (empty($results)) {
return 0.0;
}
// Calculate the average score
$totalScore = 0;
$scoreCount = count($results);
foreach ($results as $result) {
$totalScore += $result['score'];
}
return $totalScore / $scoreCount;
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class PromotionQueue extends BaseModel
{
protected $table = 'promotion_queue';
protected $primaryKey = 'id';
protected $fillable = [
'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',
];
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class PurchaseOrder extends BaseModel
{
protected $table = 'purchase_orders';
protected $primaryKey = 'id';
protected $fillable = [
'po_number',
'supplier_id',
'status',
'order_date',
'expected_date',
'subtotal',
'tax',
'total',
'notes',
];
public $timestamps = 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]',
];
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class PurchaseOrderItem extends BaseModel
{
protected $table = 'purchase_order_items';
protected $primaryKey = 'id';
protected $fillable = [
'purchase_order_id',
'supply_id',
'description',
'quantity',
'received_qty',
'unit_cost',
];
public $timestamps = 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',
];
}
+95
View File
@@ -0,0 +1,95 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Quiz extends BaseModel
{
protected $table = 'quiz';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $fillable = [
'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 $casts = [];
protected $castHandlers = [];
// Dates
public $timestamps = true;
protected $dateFormat = 'datetime';
const CREATED_AT = 'created_at';
const UPDATED_AT = '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').
* @return float The average quiz score, or 0 if no scores are found.
*/
public function getAverageQuizScore(int $studentId, string $semester, string $schoolYear): 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);
// Execute the query and get the results
$query = $builder->get();
$results = $query->getResultArray();
// If there are no scores, return 0
if (empty($results)) {
return 0.0;
}
// Calculate the average score
$totalScore = 0;
$scoreCount = count($results);
foreach ($results as $result) {
$totalScore += $result['score'];
}
return $totalScore / $scoreCount;
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Refund extends BaseModel
{
protected $table = 'refunds';
protected $primaryKey = 'id';
protected $fillable = [
'parent_id',
'school_year',
'invoice_id',
'refund_amount',
'requested_at',
'approved_at',
'refunded_at',
'status',
'reason',
'refund_paid_amount',
'request',
'note',
'approved_by',
'updated_by',
'refund_method',
'check_nbr',
'check_file',
'created_at',
'updated_at',
'semester',
];
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = '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;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use App\Models\BaseModel;
class Reimbursement extends BaseModel
{
protected $table = 'reimbursements';
protected $primaryKey = 'id';
protected $fillable = [
'expense_id',
'amount',
'reimbursed_to',
'approved_by',
'receipt_path',
'description',
'status',
'added_by',
'created_at',
'updated_at',
'school_year',
'semester',
'check_number',
'reimbursement_method',
'batch_number',
];
public $timestamps = true;
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ReimbursementBatch extends Model
{
protected $table = 'reimbursement_batches';
protected $fillable = [
'title',
'yearly_batch_number',
'status',
'school_year',
'semester',
'created_by',
'closed_by',
'opened_at',
'closed_at',
'notes',
];
public $timestamps = false;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ReimbursementBatchAdminFile extends Model
{
protected $table = 'reimbursement_batch_admin_files';
protected $fillable = [
'batch_id',
'admin_id',
'filename',
'original_filename',
'uploaded_at',
'uploaded_by',
];
public $timestamps = false;
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ReimbursementBatchItem extends Model
{
protected $table = 'reimbursement_batch_items';
protected $fillable = [
'batch_id',
'expense_id',
'reimbursement_id',
'admin_id',
'assigned_at',
'unassigned_at',
'notes',
'school_year',
'semester',
];
public $timestamps = false;
}

Some files were not shown because too many files have changed in this diff Show More